Compare commits
263 commits
s7evink/he
...
main
Author | SHA1 | Date | |
---|---|---|---|
signaryk | 73bd01f7b6 | ||
signaryk | 6a9a344d9a | ||
signaryk | 64cad580d1 | ||
signaryk | 48e3701b85 | ||
signaryk | 0c22b9ea58 | ||
signaryk | d428ae7f62 | ||
signaryk | b86c5110e1 | ||
signaryk | 09587775df | ||
signaryk | af0eadd4fe | ||
signaryk | 5f187e42d3 | ||
signaryk | 77264c3c20 | ||
signaryk | a595be09a2 | ||
signaryk | 0bfe418b18 | ||
20aa36ada7 | |||
f9c6fbab69 | |||
3e62b986d1 | |||
46902e5766 | |||
5547bf8ca6 | |||
14a6c10097 | |||
5c0ceec2a6 | |||
8aa088f713 | |||
b732eede27 | |||
ad0a7d09e8 | |||
81f73c9f8d | |||
79072c3dcd | |||
1bdf0cc541 | |||
a00b976a00 | |||
b9abbf7b20 | |||
de95499178 | |||
928c8c8c4a | |||
09f15a3d3f | |||
ad3a3e7bed | |||
66865597e2 | |||
4452833099 | |||
4892b08dd5 | |||
58bc289a37 | |||
e4a579f10f | |||
865fff5f03 | |||
4ccf6d6f67 | |||
f4e77453cb | |||
8f944f6434 | |||
ecb7b383e9 | |||
e9deb5244e | |||
be0c27e688 | |||
436773ab71 | |||
0f6b81f456 | |||
a3a18fbcce | |||
87f028db27 | |||
8f68f1ff53 | |||
a4817f31c0 | |||
00217a69d1 | |||
d58daf9665 | |||
8e4dc6b4ae | |||
d357615452 | |||
bebf701dce | |||
dae1ef2e46 | |||
3a4b5f49ac | |||
e34242008b | |||
57646d5b86 | |||
9510fa00cc | |||
13c5173273 | |||
edd02ec468 | |||
9a5a56718e | |||
f93d1c4790 | |||
d65449c782 | |||
b7054f4274 | |||
1555b3542d | |||
185ad6b00d | |||
fd11e65a9d | |||
61e5dc47d7 | |||
210bce9938 | |||
4f943771fa | |||
b8f91485b4 | |||
c4528b2de8 | |||
f25cce237e | |||
210123bab5 | |||
06e079abac | |||
fde4225469 | |||
7863a405a5 | |||
699f5ca8c1 | |||
ee73a90aea | |||
5f872f4a82 | |||
5c67eb99b3 | |||
8b4043473c | |||
da7bca0224 | |||
32f7c4b166 | |||
317b1018a3 | |||
89482ad790 | |||
a0375d41fb | |||
e02a7948d8 | |||
4fa8512d57 | |||
1b124fe9cb | |||
c1d6b9aa8e | |||
8b3adaf244 | |||
8c23c1150c | |||
fe2955a4db | |||
933ae2db91 | |||
5888329b13 | |||
2259e71c0c | |||
3d02c81031 | |||
1853f58cb4 | |||
b341a66152 | |||
4d344b65b2 | |||
f1db57c7f8 | |||
f02d998253 | |||
10b4fbc66d | |||
05a8f1ede3 | |||
16d922de70 | |||
d065219de1 | |||
db83789654 | |||
8245b24100 | |||
058081e68e | |||
bea73c765a | |||
478827459c | |||
bb2ab62cbf | |||
11fd2f019b | |||
b538f237df | |||
e3a7039c81 | |||
43b1ddb89b | |||
1c4ec67bb6 | |||
9b5be6b9c5 | |||
a721294e2b | |||
845800abfa | |||
57ddbe015d | |||
9a12420428 | |||
fa6c7ba456 | |||
35804f8493 | |||
294eff8a7f | |||
c7193e24d0 | |||
af13fa1c75 | |||
3f727485d6 | |||
79d4a0e399 | |||
7899f47e71 | |||
a48c7d33a5 | |||
c809e95335 | |||
e216c2fbf0 | |||
9582827493 | |||
297479ea49 | |||
a01faee17c | |||
33ff309572 | |||
6011ddc0a8 | |||
3e314e028e | |||
5267cc0f54 | |||
f12982472c | |||
0df982a2e5 | |||
99f94fc735 | |||
69b2069dea | |||
b965a08faa | |||
ef32de928d | |||
74a5ab6c24 | |||
eb9e90379d | |||
e93bdd56fd | |||
c08c7405db | |||
cc9b695c1e | |||
3a125fd8fa | |||
d507c5fc95 | |||
fea946d914 | |||
9f7e14e4d0 | |||
4a666932f5 | |||
e1d76de6c6 | |||
49d75d3cf6 | |||
5a87c703fa | |||
4c3a526e1b | |||
2ee03fd657 | |||
de1ed9d486 | |||
939ee325f8 | |||
23cd7877a1 | |||
4722f12fab | |||
a5ea928d0f | |||
45082d4dce | |||
a734b112c6 | |||
d13466c1ee | |||
420e7ec81f | |||
8cf6c381e2 | |||
3f4df25b31 | |||
5aaa539e3e | |||
e4665979bf | |||
7a2e325d10 | |||
2c87972a3a | |||
82b73a4906 | |||
77d9e4e93d | |||
832ccc32f6 | |||
5713c5715c | |||
8ea1a11105 | |||
7a1fd7f512 | |||
725ff5567d | |||
d11da6ec7c | |||
ea6b368ad4 | |||
cbdc601f1b | |||
61341aca50 | |||
3dcca4017c | |||
f956a8c1d9 | |||
11b557097c | |||
5d6221d191 | |||
2eae8dc489 | |||
027a9b8ce0 | |||
345f025ee3 | |||
67d6876857 | |||
0489d16f95 | |||
a49c9f01e2 | |||
2b34f88fde | |||
d5c11a3c86 | |||
99b143d4d0 | |||
6284790f98 | |||
9b98e5a102 | |||
f5b3144dc3 | |||
696cbb70b8 | |||
b00e272e6f | |||
9e9617ff84 | |||
6b47cf0f6a | |||
1432743d1a | |||
d23d0369cc | |||
6171310307 | |||
c6457cd4e5 | |||
b189edf4f4 | |||
2475cf4b61 | |||
dd5e47a9a7 | |||
ed19efc5d7 | |||
4679098a64 | |||
1647213fac | |||
71eeccf34a | |||
72285b2659 | |||
9fa39263c0 | |||
f66862958d | |||
914e6145a5 | |||
2d822356ff | |||
c45d8cd688 | |||
ca63b414da | |||
94e81cc3f3 | |||
ee57400afd | |||
0db43f13a6 | |||
e093005bc2 | |||
3691423626 | |||
985298cfc4 | |||
682a7d0a66 | |||
560ba46272 | |||
c2db38d295 | |||
4cb9cd7842 | |||
10ef1fb11a | |||
675926967d | |||
8223e1f2e1 | |||
01dd02dad2 | |||
44ed0a3279 | |||
2854ffeb7d | |||
28d3e296a8 | |||
f4104b4b5d | |||
69e3bd82a9 | |||
fa7710315a | |||
e8b2162a01 | |||
aa1bda4c58 | |||
e2d2482ca6 | |||
05f72fc4be | |||
234ed603e6 | |||
cb18ba0230 | |||
14085d30ac | |||
a4400bdd76 | |||
b741d38e10 | |||
6948d16527 | |||
5e85a00cb3 | |||
ec6879e5ae | |||
0459d2b9e5 | |||
5579121c6f | |||
d88f71ab71 |
|
@ -1,3 +1,2 @@
|
||||||
bin
|
bin
|
||||||
*.wasm
|
*.wasm
|
||||||
.git
|
|
59
.forgejo/workflows/docker.yml
Normal file
59
.forgejo/workflows/docker.yml
Normal file
|
@ -0,0 +1,59 @@
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
tags:
|
||||||
|
- 'v*'
|
||||||
|
|
||||||
|
env:
|
||||||
|
GHCR_NAMESPACE: sigb.us
|
||||||
|
PLATFORMS: linux/amd64
|
||||||
|
FORGEJO_USER: signaryk
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
monolith:
|
||||||
|
name: Monolith image
|
||||||
|
runs-on: docker
|
||||||
|
container:
|
||||||
|
image: ghcr.io/catthehacker/ubuntu:act-latest
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
packages: write
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
- name: Get release tag & build flags
|
||||||
|
if: github.event_name == 'release' # Only for GitHub releases
|
||||||
|
run: |
|
||||||
|
echo "RELEASE_VERSION=${GITHUB_REF#refs/*/}" >> $GITHUB_ENV
|
||||||
|
- name: Set up Docker Buildx
|
||||||
|
uses: docker/setup-buildx-action@v3
|
||||||
|
- name: Login to sigb.us container registry
|
||||||
|
uses: docker/login-action@v3
|
||||||
|
with:
|
||||||
|
registry: git.sigb.us
|
||||||
|
username: ${{ env.FORGEJO_USER }}
|
||||||
|
password: ${{ secrets.FORGEJO_TOKEN }}
|
||||||
|
|
||||||
|
- name: Build main monolith image
|
||||||
|
id: docker_build_monolith
|
||||||
|
uses: docker/build-push-action@v3
|
||||||
|
with:
|
||||||
|
context: .
|
||||||
|
platforms: ${{ env.PLATFORMS }}
|
||||||
|
push: true
|
||||||
|
tags: |
|
||||||
|
git.sigb.us/${{ env.GHCR_NAMESPACE }}/dendrite:${{ github.ref_name }}
|
||||||
|
git.sigb.us/${{ env.GHCR_NAMESPACE }}/dendrite:latest
|
||||||
|
git.sigb.us/${{ env.GHCR_NAMESPACE }}/dendrite:devel
|
||||||
|
|
||||||
|
- name: Build release monolith image
|
||||||
|
if: github.event_name == 'release' # Only for GitHub releases
|
||||||
|
id: docker_build_monolith_release
|
||||||
|
uses: docker/build-push-action@v3
|
||||||
|
with:
|
||||||
|
context: .
|
||||||
|
platforms: ${{ env.PLATFORMS }}
|
||||||
|
push: true
|
||||||
|
tags: |
|
||||||
|
git.sigb.us/${{ env.GHCR_NAMESPACE }}/dendrite:latest
|
||||||
|
git.sigb.us/${{ env.GHCR_NAMESPACE }}/dendrite:stable
|
||||||
|
git.sigb.us/${{ env.GHCR_NAMESPACE }}/dendrite:${{ env.RELEASE_VERSION }}
|
2
.github/ISSUE_TEMPLATE/BUG_REPORT.md
vendored
2
.github/ISSUE_TEMPLATE/BUG_REPORT.md
vendored
|
@ -62,6 +62,6 @@ If you can identify any relevant log snippets from server logs, please include
|
||||||
those (please be careful to remove any personal or private data). Please surround them with
|
those (please be careful to remove any personal or private data). Please surround them with
|
||||||
``` (three backticks, on a line on their own), so that they are formatted legibly.
|
``` (three backticks, on a line on their own), so that they are formatted legibly.
|
||||||
|
|
||||||
Alternatively, please send logs to @kegan:matrix.org or @neilalexander:matrix.org
|
Alternatively, please send logs to @kegan:matrix.org, @s7evink:matrix.org or @devonh:one.ems.host
|
||||||
with a link to the respective Github issue, thanks!
|
with a link to the respective Github issue, thanks!
|
||||||
-->
|
-->
|
||||||
|
|
2
.github/codecov.yaml
vendored
2
.github/codecov.yaml
vendored
|
@ -7,7 +7,7 @@ coverage:
|
||||||
project:
|
project:
|
||||||
default:
|
default:
|
||||||
target: auto
|
target: auto
|
||||||
threshold: 0%
|
threshold: 0.1%
|
||||||
base: auto
|
base: auto
|
||||||
flags:
|
flags:
|
||||||
- unittests
|
- unittests
|
||||||
|
|
89
.github/workflows/dendrite.yml
vendored
89
.github/workflows/dendrite.yml
vendored
|
@ -28,10 +28,10 @@ jobs:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
if: ${{ false }} # disable for now
|
if: ${{ false }} # disable for now
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
- name: Install Go
|
- name: Install Go
|
||||||
uses: actions/setup-go@v3
|
uses: actions/setup-go@v4
|
||||||
with:
|
with:
|
||||||
go-version: "stable"
|
go-version: "stable"
|
||||||
cache: true
|
cache: true
|
||||||
|
@ -41,7 +41,7 @@ jobs:
|
||||||
with:
|
with:
|
||||||
node-version: 14
|
node-version: 14
|
||||||
|
|
||||||
- uses: actions/cache@v3
|
- uses: actions/cache@v4
|
||||||
with:
|
with:
|
||||||
path: ~/.npm
|
path: ~/.npm
|
||||||
key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}
|
key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}
|
||||||
|
@ -66,9 +66,11 @@ jobs:
|
||||||
name: Linting
|
name: Linting
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v4
|
||||||
|
- name: Install libolm
|
||||||
|
run: sudo apt-get install libolm-dev libolm3
|
||||||
- name: Install Go
|
- name: Install Go
|
||||||
uses: actions/setup-go@v3
|
uses: actions/setup-go@v4
|
||||||
with:
|
with:
|
||||||
go-version: "stable"
|
go-version: "stable"
|
||||||
- name: golangci-lint
|
- name: golangci-lint
|
||||||
|
@ -100,12 +102,14 @@ jobs:
|
||||||
--health-timeout 5s
|
--health-timeout 5s
|
||||||
--health-retries 5
|
--health-retries 5
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v4
|
||||||
|
- name: Install libolm
|
||||||
|
run: sudo apt-get install libolm-dev libolm3
|
||||||
- name: Setup go
|
- name: Setup go
|
||||||
uses: actions/setup-go@v3
|
uses: actions/setup-go@v4
|
||||||
with:
|
with:
|
||||||
go-version: "stable"
|
go-version: "stable"
|
||||||
- uses: actions/cache@v3
|
- uses: actions/cache@v4
|
||||||
# manually set up caches, as they otherwise clash with different steps using setup-go with cache=true
|
# manually set up caches, as they otherwise clash with different steps using setup-go with cache=true
|
||||||
with:
|
with:
|
||||||
path: |
|
path: |
|
||||||
|
@ -119,7 +123,7 @@ jobs:
|
||||||
with:
|
with:
|
||||||
# Optional: pass GITHUB_TOKEN to avoid rate limiting.
|
# Optional: pass GITHUB_TOKEN to avoid rate limiting.
|
||||||
token: ${{ secrets.GITHUB_TOKEN }}
|
token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
- run: go test -json -v ./... 2>&1 | gotestfmt
|
- run: go test -json -v ./... 2>&1 | gotestfmt -hide all
|
||||||
env:
|
env:
|
||||||
POSTGRES_HOST: localhost
|
POSTGRES_HOST: localhost
|
||||||
POSTGRES_USER: postgres
|
POSTGRES_USER: postgres
|
||||||
|
@ -137,12 +141,12 @@ jobs:
|
||||||
goos: ["linux"]
|
goos: ["linux"]
|
||||||
goarch: ["amd64", "386"]
|
goarch: ["amd64", "386"]
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v4
|
||||||
- name: Setup go
|
- name: Setup go
|
||||||
uses: actions/setup-go@v3
|
uses: actions/setup-go@v4
|
||||||
with:
|
with:
|
||||||
go-version: "stable"
|
go-version: "stable"
|
||||||
- uses: actions/cache@v3
|
- uses: actions/cache@v4
|
||||||
with:
|
with:
|
||||||
path: |
|
path: |
|
||||||
~/.cache/go-build
|
~/.cache/go-build
|
||||||
|
@ -170,12 +174,12 @@ jobs:
|
||||||
goos: ["windows"]
|
goos: ["windows"]
|
||||||
goarch: ["amd64"]
|
goarch: ["amd64"]
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v4
|
||||||
- name: Setup Go
|
- name: Setup Go
|
||||||
uses: actions/setup-go@v3
|
uses: actions/setup-go@v4
|
||||||
with:
|
with:
|
||||||
go-version: "stable"
|
go-version: "stable"
|
||||||
- uses: actions/cache@v3
|
- uses: actions/cache@v4
|
||||||
with:
|
with:
|
||||||
path: |
|
path: |
|
||||||
~/.cache/go-build
|
~/.cache/go-build
|
||||||
|
@ -231,9 +235,11 @@ jobs:
|
||||||
--health-timeout 5s
|
--health-timeout 5s
|
||||||
--health-retries 5
|
--health-retries 5
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v4
|
||||||
|
- name: Install libolm
|
||||||
|
run: sudo apt-get install libolm-dev libolm3
|
||||||
- name: Setup go
|
- name: Setup go
|
||||||
uses: actions/setup-go@v3
|
uses: actions/setup-go@v4
|
||||||
with:
|
with:
|
||||||
go-version: "stable"
|
go-version: "stable"
|
||||||
- name: Set up gotestfmt
|
- name: Set up gotestfmt
|
||||||
|
@ -241,7 +247,7 @@ jobs:
|
||||||
with:
|
with:
|
||||||
# Optional: pass GITHUB_TOKEN to avoid rate limiting.
|
# Optional: pass GITHUB_TOKEN to avoid rate limiting.
|
||||||
token: ${{ secrets.GITHUB_TOKEN }}
|
token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
- uses: actions/cache@v3
|
- uses: actions/cache@v4
|
||||||
with:
|
with:
|
||||||
path: |
|
path: |
|
||||||
~/.cache/go-build
|
~/.cache/go-build
|
||||||
|
@ -249,17 +255,18 @@ jobs:
|
||||||
key: ${{ runner.os }}-go-stable-test-race-${{ hashFiles('**/go.sum') }}
|
key: ${{ runner.os }}-go-stable-test-race-${{ hashFiles('**/go.sum') }}
|
||||||
restore-keys: |
|
restore-keys: |
|
||||||
${{ runner.os }}-go-stable-test-race-
|
${{ runner.os }}-go-stable-test-race-
|
||||||
- run: go test -race -json -v -coverpkg=./... -coverprofile=cover.out $(go list ./... | grep -v /cmd/dendrite*) 2>&1 | gotestfmt
|
- run: go test -race -json -v -coverpkg=./... -coverprofile=cover.out $(go list ./... | grep -v /cmd/dendrite*) 2>&1 | gotestfmt -hide all
|
||||||
env:
|
env:
|
||||||
POSTGRES_HOST: localhost
|
POSTGRES_HOST: localhost
|
||||||
POSTGRES_USER: postgres
|
POSTGRES_USER: postgres
|
||||||
POSTGRES_PASSWORD: postgres
|
POSTGRES_PASSWORD: postgres
|
||||||
POSTGRES_DB: dendrite
|
POSTGRES_DB: dendrite
|
||||||
- name: Upload coverage to Codecov
|
- name: Upload coverage to Codecov
|
||||||
uses: codecov/codecov-action@v3
|
uses: codecov/codecov-action@v4
|
||||||
with:
|
with:
|
||||||
flags: unittests
|
flags: unittests
|
||||||
fail_ci_if_error: true
|
fail_ci_if_error: true
|
||||||
|
token: ${{ secrets.CODECOV_TOKEN }}
|
||||||
|
|
||||||
# run database upgrade tests
|
# run database upgrade tests
|
||||||
upgrade_test:
|
upgrade_test:
|
||||||
|
@ -268,12 +275,22 @@ jobs:
|
||||||
needs: initial-tests-done
|
needs: initial-tests-done
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v4
|
||||||
- name: Setup go
|
- name: Setup go
|
||||||
uses: actions/setup-go@v3
|
uses: actions/setup-go@v4
|
||||||
with:
|
with:
|
||||||
go-version: "stable"
|
go-version: "stable"
|
||||||
cache: true
|
cache: true
|
||||||
|
- uses: actions/cache@v4
|
||||||
|
with:
|
||||||
|
path: |
|
||||||
|
~/.cache/go-build
|
||||||
|
~/go/pkg/mod
|
||||||
|
key: ${{ runner.os }}-go-upgrade-test-${{ hashFiles('**/go.sum') }}
|
||||||
|
restore-keys: |
|
||||||
|
${{ runner.os }}-go-upgrade-test-
|
||||||
|
- name: Docker version
|
||||||
|
run: docker version
|
||||||
- name: Build upgrade-tests
|
- name: Build upgrade-tests
|
||||||
run: go build ./cmd/dendrite-upgrade-tests
|
run: go build ./cmd/dendrite-upgrade-tests
|
||||||
- name: Test upgrade (PostgreSQL)
|
- name: Test upgrade (PostgreSQL)
|
||||||
|
@ -288,12 +305,22 @@ jobs:
|
||||||
needs: initial-tests-done
|
needs: initial-tests-done
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v4
|
||||||
- name: Setup go
|
- name: Setup go
|
||||||
uses: actions/setup-go@v3
|
uses: actions/setup-go@v4
|
||||||
with:
|
with:
|
||||||
go-version: "stable"
|
go-version: "stable"
|
||||||
cache: true
|
cache: true
|
||||||
|
- uses: actions/cache@v4
|
||||||
|
with:
|
||||||
|
path: |
|
||||||
|
~/.cache/go-build
|
||||||
|
~/go/pkg/mod
|
||||||
|
key: ${{ runner.os }}-go-upgrade-direct-test-${{ hashFiles('**/go.sum') }}
|
||||||
|
restore-keys: |
|
||||||
|
${{ runner.os }}-go-upgrade-direct-test-
|
||||||
|
- name: Docker version
|
||||||
|
run: docker version
|
||||||
- name: Build upgrade-tests
|
- name: Build upgrade-tests
|
||||||
run: go build ./cmd/dendrite-upgrade-tests
|
run: go build ./cmd/dendrite-upgrade-tests
|
||||||
- name: Test upgrade (PostgreSQL)
|
- name: Test upgrade (PostgreSQL)
|
||||||
|
@ -330,8 +357,8 @@ jobs:
|
||||||
SYTEST_BRANCH: ${{ github.head_ref }}
|
SYTEST_BRANCH: ${{ github.head_ref }}
|
||||||
CGO_ENABLED: ${{ matrix.cgo && 1 }}
|
CGO_ENABLED: ${{ matrix.cgo && 1 }}
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v4
|
||||||
- uses: actions/cache@v3
|
- uses: actions/cache@v4
|
||||||
with:
|
with:
|
||||||
path: |
|
path: |
|
||||||
~/.cache/go-build
|
~/.cache/go-build
|
||||||
|
@ -354,7 +381,7 @@ jobs:
|
||||||
run: /src/are-we-synapse-yet.py /logs/results.tap -v
|
run: /src/are-we-synapse-yet.py /logs/results.tap -v
|
||||||
continue-on-error: true # not fatal
|
continue-on-error: true # not fatal
|
||||||
- name: Upload Sytest logs
|
- name: Upload Sytest logs
|
||||||
uses: actions/upload-artifact@v2
|
uses: actions/upload-artifact@v4
|
||||||
if: ${{ always() }}
|
if: ${{ always() }}
|
||||||
with:
|
with:
|
||||||
name: Sytest Logs - ${{ job.status }} - (Dendrite, ${{ join(matrix.*, ', ') }})
|
name: Sytest Logs - ${{ job.status }} - (Dendrite, ${{ join(matrix.*, ', ') }})
|
||||||
|
@ -393,9 +420,9 @@ jobs:
|
||||||
# See https://github.com/actions/virtual-environments/blob/main/images/linux/Ubuntu2004-Readme.md specifically GOROOT_1_17_X64
|
# See https://github.com/actions/virtual-environments/blob/main/images/linux/Ubuntu2004-Readme.md specifically GOROOT_1_17_X64
|
||||||
run: |
|
run: |
|
||||||
sudo apt-get update && sudo apt-get install -y libolm3 libolm-dev
|
sudo apt-get update && sudo apt-get install -y libolm3 libolm-dev
|
||||||
go get -v github.com/gotesttools/gotestfmt/v2/cmd/gotestfmt@latest
|
go install github.com/gotesttools/gotestfmt/v2/cmd/gotestfmt@latest
|
||||||
- name: Run actions/checkout@v3 for dendrite
|
- name: Run actions/checkout@v4 for dendrite
|
||||||
uses: actions/checkout@v3
|
uses: actions/checkout@v4
|
||||||
with:
|
with:
|
||||||
path: dendrite
|
path: dendrite
|
||||||
|
|
||||||
|
@ -430,7 +457,7 @@ jobs:
|
||||||
# Run Complement
|
# Run Complement
|
||||||
- run: |
|
- run: |
|
||||||
set -o pipefail &&
|
set -o pipefail &&
|
||||||
go test -v -json -tags dendrite_blacklist ./tests/... 2>&1 | gotestfmt
|
go test -v -json -tags dendrite_blacklist ./tests ./tests/csapi 2>&1 | gotestfmt -hide all
|
||||||
shell: bash
|
shell: bash
|
||||||
name: Run Complement Tests
|
name: Run Complement Tests
|
||||||
env:
|
env:
|
||||||
|
|
52
.github/workflows/docker.yml
vendored
52
.github/workflows/docker.yml
vendored
|
@ -27,26 +27,22 @@ jobs:
|
||||||
security-events: write # To upload Trivy sarif files
|
security-events: write # To upload Trivy sarif files
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v3
|
uses: actions/checkout@v4
|
||||||
- name: Get release tag & build flags
|
- name: Get release tag & build flags
|
||||||
if: github.event_name == 'release' # Only for GitHub releases
|
if: github.event_name == 'release' # Only for GitHub releases
|
||||||
run: |
|
run: |
|
||||||
echo "RELEASE_VERSION=${GITHUB_REF#refs/*/}" >> $GITHUB_ENV
|
echo "RELEASE_VERSION=${GITHUB_REF#refs/*/}" >> $GITHUB_ENV
|
||||||
echo "BUILD=$(git rev-parse --short HEAD || \"\")" >> $GITHUB_ENV
|
|
||||||
BRANCH=$(git symbolic-ref --short HEAD | tr -d \/)
|
|
||||||
[ ${BRANCH} == "main" ] && BRANCH=""
|
|
||||||
echo "BRANCH=${BRANCH}" >> $GITHUB_ENV
|
|
||||||
- name: Set up QEMU
|
- name: Set up QEMU
|
||||||
uses: docker/setup-qemu-action@v1
|
uses: docker/setup-qemu-action@v3
|
||||||
- name: Set up Docker Buildx
|
- name: Set up Docker Buildx
|
||||||
uses: docker/setup-buildx-action@v2
|
uses: docker/setup-buildx-action@v3
|
||||||
- name: Login to Docker Hub
|
- name: Login to Docker Hub
|
||||||
uses: docker/login-action@v2
|
uses: docker/login-action@v3
|
||||||
with:
|
with:
|
||||||
username: ${{ env.DOCKER_HUB_USER }}
|
username: ${{ env.DOCKER_HUB_USER }}
|
||||||
password: ${{ secrets.DOCKER_TOKEN }}
|
password: ${{ secrets.DOCKER_TOKEN }}
|
||||||
- name: Login to GitHub Containers
|
- name: Login to GitHub Containers
|
||||||
uses: docker/login-action@v2
|
uses: docker/login-action@v3
|
||||||
with:
|
with:
|
||||||
registry: ghcr.io
|
registry: ghcr.io
|
||||||
username: ${{ github.repository_owner }}
|
username: ${{ github.repository_owner }}
|
||||||
|
@ -57,10 +53,9 @@ jobs:
|
||||||
id: docker_build_monolith
|
id: docker_build_monolith
|
||||||
uses: docker/build-push-action@v3
|
uses: docker/build-push-action@v3
|
||||||
with:
|
with:
|
||||||
cache-from: type=gha
|
cache-from: type=registry,ref=ghcr.io/${{ env.GHCR_NAMESPACE }}/dendrite-monolith:buildcache
|
||||||
cache-to: type=gha,mode=max
|
cache-to: type=registry,ref=ghcr.io/${{ env.GHCR_NAMESPACE }}/dendrite-monolith:buildcache,mode=max
|
||||||
context: .
|
context: .
|
||||||
build-args: FLAGS=-X github.com/matrix-org/dendrite/internal.branch=${{ env.BRANCH }} -X github.com/matrix-org/dendrite/internal.build=${{ env.BUILD }}
|
|
||||||
platforms: ${{ env.PLATFORMS }}
|
platforms: ${{ env.PLATFORMS }}
|
||||||
push: true
|
push: true
|
||||||
tags: |
|
tags: |
|
||||||
|
@ -75,7 +70,6 @@ jobs:
|
||||||
cache-from: type=gha
|
cache-from: type=gha
|
||||||
cache-to: type=gha,mode=max
|
cache-to: type=gha,mode=max
|
||||||
context: .
|
context: .
|
||||||
build-args: FLAGS=-X github.com/matrix-org/dendrite/internal.branch=${{ env.BRANCH }} -X github.com/matrix-org/dendrite/internal.build=${{ env.BUILD }}
|
|
||||||
platforms: ${{ env.PLATFORMS }}
|
platforms: ${{ env.PLATFORMS }}
|
||||||
push: true
|
push: true
|
||||||
tags: |
|
tags: |
|
||||||
|
@ -104,26 +98,22 @@ jobs:
|
||||||
packages: write
|
packages: write
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v3
|
uses: actions/checkout@v4
|
||||||
- name: Get release tag & build flags
|
- name: Get release tag & build flags
|
||||||
if: github.event_name == 'release' # Only for GitHub releases
|
if: github.event_name == 'release' # Only for GitHub releases
|
||||||
run: |
|
run: |
|
||||||
echo "RELEASE_VERSION=${GITHUB_REF#refs/*/}" >> $GITHUB_ENV
|
echo "RELEASE_VERSION=${GITHUB_REF#refs/*/}" >> $GITHUB_ENV
|
||||||
echo "BUILD=$(git rev-parse --short HEAD || \"\")" >> $GITHUB_ENV
|
|
||||||
BRANCH=$(git symbolic-ref --short HEAD | tr -d \/)
|
|
||||||
[ ${BRANCH} == "main" ] && BRANCH=""
|
|
||||||
echo "BRANCH=${BRANCH}" >> $GITHUB_ENV
|
|
||||||
- name: Set up QEMU
|
- name: Set up QEMU
|
||||||
uses: docker/setup-qemu-action@v1
|
uses: docker/setup-qemu-action@v3
|
||||||
- name: Set up Docker Buildx
|
- name: Set up Docker Buildx
|
||||||
uses: docker/setup-buildx-action@v2
|
uses: docker/setup-buildx-action@v3
|
||||||
- name: Login to Docker Hub
|
- name: Login to Docker Hub
|
||||||
uses: docker/login-action@v2
|
uses: docker/login-action@v3
|
||||||
with:
|
with:
|
||||||
username: ${{ env.DOCKER_HUB_USER }}
|
username: ${{ env.DOCKER_HUB_USER }}
|
||||||
password: ${{ secrets.DOCKER_TOKEN }}
|
password: ${{ secrets.DOCKER_TOKEN }}
|
||||||
- name: Login to GitHub Containers
|
- name: Login to GitHub Containers
|
||||||
uses: docker/login-action@v2
|
uses: docker/login-action@v3
|
||||||
with:
|
with:
|
||||||
registry: ghcr.io
|
registry: ghcr.io
|
||||||
username: ${{ github.repository_owner }}
|
username: ${{ github.repository_owner }}
|
||||||
|
@ -137,7 +127,6 @@ jobs:
|
||||||
cache-from: type=gha
|
cache-from: type=gha
|
||||||
cache-to: type=gha,mode=max
|
cache-to: type=gha,mode=max
|
||||||
context: .
|
context: .
|
||||||
build-args: FLAGS=-X github.com/matrix-org/dendrite/internal.branch=${{ env.BRANCH }} -X github.com/matrix-org/dendrite/internal.build=${{ env.BUILD }}
|
|
||||||
file: ./build/docker/Dockerfile.demo-pinecone
|
file: ./build/docker/Dockerfile.demo-pinecone
|
||||||
platforms: ${{ env.PLATFORMS }}
|
platforms: ${{ env.PLATFORMS }}
|
||||||
push: true
|
push: true
|
||||||
|
@ -153,7 +142,6 @@ jobs:
|
||||||
cache-from: type=gha
|
cache-from: type=gha
|
||||||
cache-to: type=gha,mode=max
|
cache-to: type=gha,mode=max
|
||||||
context: .
|
context: .
|
||||||
build-args: FLAGS=-X github.com/matrix-org/dendrite/internal.branch=${{ env.BRANCH }} -X github.com/matrix-org/dendrite/internal.build=${{ env.BUILD }}
|
|
||||||
file: ./build/docker/Dockerfile.demo-pinecone
|
file: ./build/docker/Dockerfile.demo-pinecone
|
||||||
platforms: ${{ env.PLATFORMS }}
|
platforms: ${{ env.PLATFORMS }}
|
||||||
push: true
|
push: true
|
||||||
|
@ -171,26 +159,22 @@ jobs:
|
||||||
packages: write
|
packages: write
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v3
|
uses: actions/checkout@v4
|
||||||
- name: Get release tag & build flags
|
- name: Get release tag & build flags
|
||||||
if: github.event_name == 'release' # Only for GitHub releases
|
if: github.event_name == 'release' # Only for GitHub releases
|
||||||
run: |
|
run: |
|
||||||
echo "RELEASE_VERSION=${GITHUB_REF#refs/*/}" >> $GITHUB_ENV
|
echo "RELEASE_VERSION=${GITHUB_REF#refs/*/}" >> $GITHUB_ENV
|
||||||
echo "BUILD=$(git rev-parse --short HEAD || \"\")" >> $GITHUB_ENV
|
|
||||||
BRANCH=$(git symbolic-ref --short HEAD | tr -d \/)
|
|
||||||
[ ${BRANCH} == "main" ] && BRANCH=""
|
|
||||||
echo "BRANCH=${BRANCH}" >> $GITHUB_ENV
|
|
||||||
- name: Set up QEMU
|
- name: Set up QEMU
|
||||||
uses: docker/setup-qemu-action@v1
|
uses: docker/setup-qemu-action@v3
|
||||||
- name: Set up Docker Buildx
|
- name: Set up Docker Buildx
|
||||||
uses: docker/setup-buildx-action@v2
|
uses: docker/setup-buildx-action@v3
|
||||||
- name: Login to Docker Hub
|
- name: Login to Docker Hub
|
||||||
uses: docker/login-action@v2
|
uses: docker/login-action@v3
|
||||||
with:
|
with:
|
||||||
username: ${{ env.DOCKER_HUB_USER }}
|
username: ${{ env.DOCKER_HUB_USER }}
|
||||||
password: ${{ secrets.DOCKER_TOKEN }}
|
password: ${{ secrets.DOCKER_TOKEN }}
|
||||||
- name: Login to GitHub Containers
|
- name: Login to GitHub Containers
|
||||||
uses: docker/login-action@v2
|
uses: docker/login-action@v3
|
||||||
with:
|
with:
|
||||||
registry: ghcr.io
|
registry: ghcr.io
|
||||||
username: ${{ github.repository_owner }}
|
username: ${{ github.repository_owner }}
|
||||||
|
@ -204,7 +188,6 @@ jobs:
|
||||||
cache-from: type=gha
|
cache-from: type=gha
|
||||||
cache-to: type=gha,mode=max
|
cache-to: type=gha,mode=max
|
||||||
context: .
|
context: .
|
||||||
build-args: FLAGS=-X github.com/matrix-org/dendrite/internal.branch=${{ env.BRANCH }} -X github.com/matrix-org/dendrite/internal.build=${{ env.BUILD }}
|
|
||||||
file: ./build/docker/Dockerfile.demo-yggdrasil
|
file: ./build/docker/Dockerfile.demo-yggdrasil
|
||||||
platforms: ${{ env.PLATFORMS }}
|
platforms: ${{ env.PLATFORMS }}
|
||||||
push: true
|
push: true
|
||||||
|
@ -220,7 +203,6 @@ jobs:
|
||||||
cache-from: type=gha
|
cache-from: type=gha
|
||||||
cache-to: type=gha,mode=max
|
cache-to: type=gha,mode=max
|
||||||
context: .
|
context: .
|
||||||
build-args: FLAGS=-X github.com/matrix-org/dendrite/internal.branch=${{ env.BRANCH }} -X github.com/matrix-org/dendrite/internal.build=${{ env.BUILD }}
|
|
||||||
file: ./build/docker/Dockerfile.demo-yggdrasil
|
file: ./build/docker/Dockerfile.demo-yggdrasil
|
||||||
platforms: ${{ env.PLATFORMS }}
|
platforms: ${{ env.PLATFORMS }}
|
||||||
push: true
|
push: true
|
||||||
|
|
2
.github/workflows/gh-pages.yml
vendored
2
.github/workflows/gh-pages.yml
vendored
|
@ -28,7 +28,7 @@ jobs:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v3
|
uses: actions/checkout@v4
|
||||||
- name: Setup Pages
|
- name: Setup Pages
|
||||||
uses: actions/configure-pages@v2
|
uses: actions/configure-pages@v2
|
||||||
- name: Build with Jekyll
|
- name: Build with Jekyll
|
||||||
|
|
6
.github/workflows/helm.yml
vendored
6
.github/workflows/helm.yml
vendored
|
@ -6,6 +6,7 @@ on:
|
||||||
- main
|
- main
|
||||||
paths:
|
paths:
|
||||||
- 'helm/**' # only execute if we have helm chart changes
|
- 'helm/**' # only execute if we have helm chart changes
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
release:
|
release:
|
||||||
|
@ -16,7 +17,7 @@ jobs:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v2
|
uses: actions/checkout@v4
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
|
|
||||||
|
@ -31,9 +32,10 @@ jobs:
|
||||||
version: v3.10.0
|
version: v3.10.0
|
||||||
|
|
||||||
- name: Run chart-releaser
|
- name: Run chart-releaser
|
||||||
uses: helm/chart-releaser-action@v1.4.1
|
uses: helm/chart-releaser-action@ed43eb303604cbc0eeec8390544f7748dc6c790d # specific commit, since `mark_as_latest` is not yet in a release
|
||||||
env:
|
env:
|
||||||
CR_TOKEN: "${{ secrets.GITHUB_TOKEN }}"
|
CR_TOKEN: "${{ secrets.GITHUB_TOKEN }}"
|
||||||
with:
|
with:
|
||||||
config: helm/cr.yaml
|
config: helm/cr.yaml
|
||||||
charts_dir: helm/
|
charts_dir: helm/
|
||||||
|
mark_as_latest: false
|
||||||
|
|
7
.github/workflows/k8s.yml
vendored
7
.github/workflows/k8s.yml
vendored
|
@ -17,7 +17,7 @@ jobs:
|
||||||
outputs:
|
outputs:
|
||||||
changed: ${{ steps.list-changed.outputs.changed }}
|
changed: ${{ steps.list-changed.outputs.changed }}
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v4
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
- uses: azure/setup-helm@v3
|
- uses: azure/setup-helm@v3
|
||||||
|
@ -48,7 +48,7 @@ jobs:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v3
|
uses: actions/checkout@v4
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
ref: ${{ inputs.checkoutCommit }}
|
ref: ${{ inputs.checkoutCommit }}
|
||||||
|
@ -66,7 +66,7 @@ jobs:
|
||||||
- name: Create k3d cluster
|
- name: Create k3d cluster
|
||||||
uses: nolar/setup-k3d-k3s@v1
|
uses: nolar/setup-k3d-k3s@v1
|
||||||
with:
|
with:
|
||||||
version: v1.21
|
version: v1.28
|
||||||
- name: Remove node taints
|
- name: Remove node taints
|
||||||
run: |
|
run: |
|
||||||
kubectl taint --all=true nodes node.cloudprovider.kubernetes.io/uninitialized- || true
|
kubectl taint --all=true nodes node.cloudprovider.kubernetes.io/uninitialized- || true
|
||||||
|
@ -84,6 +84,7 @@ jobs:
|
||||||
kubectl get pods -A
|
kubectl get pods -A
|
||||||
kubectl get services
|
kubectl get services
|
||||||
kubectl get ingress
|
kubectl get ingress
|
||||||
|
kubectl logs -l app.kubernetes.io/name=dendrite
|
||||||
- name: Run create account
|
- name: Run create account
|
||||||
run: |
|
run: |
|
||||||
podName=$(kubectl get pods -l app.kubernetes.io/name=dendrite -o name)
|
podName=$(kubectl get pods -l app.kubernetes.io/name=dendrite -o name)
|
||||||
|
|
138
.github/workflows/schedules.yaml
vendored
138
.github/workflows/schedules.yaml
vendored
|
@ -10,8 +10,26 @@ concurrency:
|
||||||
cancel-in-progress: true
|
cancel-in-progress: true
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
|
check_date: # https://stackoverflow.com/questions/63014786/how-to-schedule-a-github-actions-nightly-build-but-run-it-only-when-there-where
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
name: Check latest commit
|
||||||
|
outputs:
|
||||||
|
should_run: ${{ steps.should_run.outputs.should_run }}
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
- name: print latest_commit
|
||||||
|
run: echo ${{ github.sha }}
|
||||||
|
|
||||||
|
- id: should_run
|
||||||
|
continue-on-error: true
|
||||||
|
name: check latest commit is less than a day
|
||||||
|
if: ${{ github.event_name == 'schedule' }}
|
||||||
|
run: test -z $(git rev-list --after="24 hours" ${{ github.sha }}) && echo "::set-output name=should_run::false"
|
||||||
|
|
||||||
# run Sytest in different variations
|
# run Sytest in different variations
|
||||||
sytest:
|
sytest:
|
||||||
|
needs: check_date
|
||||||
|
if: ${{ needs.check_date.outputs.should_run != 'false' }}
|
||||||
timeout-minutes: 60
|
timeout-minutes: 60
|
||||||
name: "Sytest (${{ matrix.label }})"
|
name: "Sytest (${{ matrix.label }})"
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
@ -38,8 +56,8 @@ jobs:
|
||||||
RACE_DETECTION: 1
|
RACE_DETECTION: 1
|
||||||
COVER: 1
|
COVER: 1
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v4
|
||||||
- uses: actions/cache@v3
|
- uses: actions/cache@v4
|
||||||
with:
|
with:
|
||||||
path: |
|
path: |
|
||||||
~/.cache/go-build
|
~/.cache/go-build
|
||||||
|
@ -62,44 +80,47 @@ jobs:
|
||||||
run: /src/are-we-synapse-yet.py /logs/results.tap -v
|
run: /src/are-we-synapse-yet.py /logs/results.tap -v
|
||||||
continue-on-error: true # not fatal
|
continue-on-error: true # not fatal
|
||||||
- name: Upload Sytest logs
|
- name: Upload Sytest logs
|
||||||
uses: actions/upload-artifact@v2
|
uses: actions/upload-artifact@v4
|
||||||
if: ${{ always() }}
|
if: ${{ always() }}
|
||||||
with:
|
with:
|
||||||
name: Sytest Logs - ${{ job.status }} - (Dendrite, ${{ join(matrix.*, ', ') }})
|
name: Sytest Logs - ${{ job.status }} - (Dendrite ${{ join(matrix.*, ' ') }})
|
||||||
path: |
|
path: |
|
||||||
/logs/results.tap
|
/logs/results.tap
|
||||||
/logs/**/*.log*
|
/logs/**/*.log*
|
||||||
|
/logs/**/covdatafiles/**
|
||||||
|
|
||||||
sytest-coverage:
|
sytest-coverage:
|
||||||
timeout-minutes: 5
|
timeout-minutes: 5
|
||||||
name: "Sytest Coverage"
|
name: "Sytest Coverage"
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
needs: sytest # only run once Sytest is done
|
needs: [ sytest, check_date ] # only run once Sytest is done and there was a commit
|
||||||
if: ${{ always() }}
|
if: ${{ always() && needs.check_date.outputs.should_run != 'false' }}
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v4
|
||||||
- name: Install Go
|
- name: Install Go
|
||||||
uses: actions/setup-go@v3
|
uses: actions/setup-go@v4
|
||||||
with:
|
with:
|
||||||
go-version: 'stable'
|
go-version: 'stable'
|
||||||
cache: true
|
cache: true
|
||||||
- name: Download all artifacts
|
- name: Download all artifacts
|
||||||
uses: actions/download-artifact@v3
|
uses: actions/download-artifact@v4
|
||||||
- name: Install gocovmerge
|
- name: Collect coverage
|
||||||
run: go install github.com/wadey/gocovmerge@latest
|
|
||||||
- name: Run gocovmerge
|
|
||||||
run: |
|
run: |
|
||||||
find -name 'integrationcover.log' -printf '"%p"\n' | xargs gocovmerge | grep -Ev 'relayapi|setup/mscs|api_trace' > sytest.cov
|
go tool covdata textfmt -i="$(find Sytest* -name 'covmeta*' -type f -exec dirname {} \; | uniq | paste -s -d ',' -)" -o sytest.cov
|
||||||
go tool cover -func=sytest.cov
|
grep -Ev 'relayapi|setup/mscs|api_trace' sytest.cov > final.cov
|
||||||
|
go tool covdata func -i="$(find Sytest* -name 'covmeta*' -type f -exec dirname {} \; | uniq | paste -s -d ',' -)"
|
||||||
- name: Upload coverage to Codecov
|
- name: Upload coverage to Codecov
|
||||||
uses: codecov/codecov-action@v3
|
uses: codecov/codecov-action@v4
|
||||||
with:
|
with:
|
||||||
files: ./sytest.cov
|
files: ./final.cov
|
||||||
flags: sytest
|
flags: sytest
|
||||||
fail_ci_if_error: true
|
fail_ci_if_error: true
|
||||||
|
token: ${{ secrets.CODECOV_TOKEN }}
|
||||||
|
|
||||||
# run Complement
|
# run Complement
|
||||||
complement:
|
complement:
|
||||||
|
needs: check_date
|
||||||
|
if: ${{ needs.check_date.outputs.should_run != 'false' }}
|
||||||
name: "Complement (${{ matrix.label }})"
|
name: "Complement (${{ matrix.label }})"
|
||||||
timeout-minutes: 60
|
timeout-minutes: 60
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
@ -128,9 +149,9 @@ jobs:
|
||||||
# See https://github.com/actions/virtual-environments/blob/main/images/linux/Ubuntu2004-Readme.md specifically GOROOT_1_17_X64
|
# See https://github.com/actions/virtual-environments/blob/main/images/linux/Ubuntu2004-Readme.md specifically GOROOT_1_17_X64
|
||||||
run: |
|
run: |
|
||||||
sudo apt-get update && sudo apt-get install -y libolm3 libolm-dev
|
sudo apt-get update && sudo apt-get install -y libolm3 libolm-dev
|
||||||
go get -v github.com/gotesttools/gotestfmt/v2/cmd/gotestfmt@latest
|
go install github.com/gotesttools/gotestfmt/v2/cmd/gotestfmt@latest
|
||||||
- name: Run actions/checkout@v3 for dendrite
|
- name: Run actions/checkout@v4 for dendrite
|
||||||
uses: actions/checkout@v3
|
uses: actions/checkout@v4
|
||||||
with:
|
with:
|
||||||
path: dendrite
|
path: dendrite
|
||||||
|
|
||||||
|
@ -167,14 +188,14 @@ jobs:
|
||||||
cat <<EOF > /tmp/posttest.sh
|
cat <<EOF > /tmp/posttest.sh
|
||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
mkdir -p /tmp/Complement/logs/\$2/\$1/
|
mkdir -p /tmp/Complement/logs/\$2/\$1/
|
||||||
docker cp \$1:/dendrite/complementcover.log /tmp/Complement/logs/\$2/\$1/
|
docker cp \$1:/tmp/covdatafiles/. /tmp/Complement/logs/\$2/\$1/
|
||||||
EOF
|
EOF
|
||||||
|
|
||||||
chmod +x /tmp/posttest.sh
|
chmod +x /tmp/posttest.sh
|
||||||
# Run Complement
|
# Run Complement
|
||||||
- run: |
|
- run: |
|
||||||
set -o pipefail &&
|
set -o pipefail &&
|
||||||
go test -v -json -tags dendrite_blacklist ./tests/... 2>&1 | gotestfmt
|
go test -v -json -tags dendrite_blacklist ./tests ./tests/csapi 2>&1 | gotestfmt -hide all
|
||||||
shell: bash
|
shell: bash
|
||||||
name: Run Complement Tests
|
name: Run Complement Tests
|
||||||
env:
|
env:
|
||||||
|
@ -185,42 +206,43 @@ jobs:
|
||||||
working-directory: complement
|
working-directory: complement
|
||||||
|
|
||||||
- name: Upload Complement logs
|
- name: Upload Complement logs
|
||||||
uses: actions/upload-artifact@v2
|
uses: actions/upload-artifact@v4
|
||||||
if: ${{ always() }}
|
if: ${{ always() }}
|
||||||
with:
|
with:
|
||||||
name: Complement Logs - (Dendrite, ${{ join(matrix.*, ', ') }})
|
name: Complement Logs - (Dendrite ${{ join(matrix.*, ' ') }})
|
||||||
path: |
|
path: |
|
||||||
/tmp/Complement/**/complementcover.log
|
/tmp/Complement/logs/**
|
||||||
|
|
||||||
complement-coverage:
|
complement-coverage:
|
||||||
timeout-minutes: 5
|
timeout-minutes: 5
|
||||||
name: "Complement Coverage"
|
name: "Complement Coverage"
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
needs: complement # only run once Complement is done
|
needs: [ complement, check_date ] # only run once Complements is done and there was a commit
|
||||||
if: ${{ always() }}
|
if: ${{ always() && needs.check_date.outputs.should_run != 'false' }}
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v4
|
||||||
- name: Install Go
|
- name: Install Go
|
||||||
uses: actions/setup-go@v3
|
uses: actions/setup-go@v4
|
||||||
with:
|
with:
|
||||||
go-version: 'stable'
|
go-version: 'stable'
|
||||||
cache: true
|
cache: true
|
||||||
- name: Download all artifacts
|
- name: Download all artifacts
|
||||||
uses: actions/download-artifact@v3
|
uses: actions/download-artifact@v4
|
||||||
- name: Install gocovmerge
|
- name: Collect coverage
|
||||||
run: go install github.com/wadey/gocovmerge@latest
|
|
||||||
- name: Run gocovmerge
|
|
||||||
run: |
|
run: |
|
||||||
find -name 'complementcover.log' -printf '"%p"\n' | xargs gocovmerge | grep -Ev 'relayapi|setup/mscs|api_trace' > complement.cov
|
go tool covdata textfmt -i="$(find Complement* -name 'covmeta*' -type f -exec dirname {} \; | uniq | paste -s -d ',' -)" -o complement.cov
|
||||||
go tool cover -func=complement.cov
|
grep -Ev 'relayapi|setup/mscs|api_trace' complement.cov > final.cov
|
||||||
|
go tool covdata func -i="$(find Complement* -name 'covmeta*' -type f -exec dirname {} \; | uniq | paste -s -d ',' -)"
|
||||||
- name: Upload coverage to Codecov
|
- name: Upload coverage to Codecov
|
||||||
uses: codecov/codecov-action@v3
|
uses: codecov/codecov-action@v4
|
||||||
with:
|
with:
|
||||||
files: ./complement.cov
|
files: ./final.cov
|
||||||
flags: complement
|
flags: complement
|
||||||
fail_ci_if_error: true
|
fail_ci_if_error: true
|
||||||
|
token: ${{ secrets.CODECOV_TOKEN }} # required
|
||||||
|
|
||||||
element_web:
|
element-web:
|
||||||
|
if: ${{ false }} # disable for now, as Cypress has been replaced by Playwright
|
||||||
timeout-minutes: 120
|
timeout-minutes: 120
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
|
@ -229,7 +251,7 @@ jobs:
|
||||||
# Our test suite includes some screenshot tests with unusual diacritics, which are
|
# Our test suite includes some screenshot tests with unusual diacritics, which are
|
||||||
# supposed to be covered by STIXGeneral.
|
# supposed to be covered by STIXGeneral.
|
||||||
tools: fonts-stix
|
tools: fonts-stix
|
||||||
- uses: actions/checkout@v2
|
- uses: actions/checkout@v4
|
||||||
with:
|
with:
|
||||||
repository: matrix-org/matrix-react-sdk
|
repository: matrix-org/matrix-react-sdk
|
||||||
- uses: actions/setup-node@v3
|
- uses: actions/setup-node@v3
|
||||||
|
@ -258,3 +280,43 @@ jobs:
|
||||||
env:
|
env:
|
||||||
PUPPETEER_SKIP_CHROMIUM_DOWNLOAD: true
|
PUPPETEER_SKIP_CHROMIUM_DOWNLOAD: true
|
||||||
TMPDIR: ${{ runner.temp }}
|
TMPDIR: ${{ runner.temp }}
|
||||||
|
|
||||||
|
element-web-pinecone:
|
||||||
|
if: ${{ false }} # disable for now, as Cypress has been replaced by Playwright
|
||||||
|
timeout-minutes: 120
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: tecolicom/actions-use-apt-tools@v1
|
||||||
|
with:
|
||||||
|
# Our test suite includes some screenshot tests with unusual diacritics, which are
|
||||||
|
# supposed to be covered by STIXGeneral.
|
||||||
|
tools: fonts-stix
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
repository: matrix-org/matrix-react-sdk
|
||||||
|
- uses: actions/setup-node@v3
|
||||||
|
with:
|
||||||
|
cache: 'yarn'
|
||||||
|
- name: Fetch layered build
|
||||||
|
run: scripts/ci/layered.sh
|
||||||
|
- name: Copy config
|
||||||
|
run: cp element.io/develop/config.json config.json
|
||||||
|
working-directory: ./element-web
|
||||||
|
- name: Build
|
||||||
|
env:
|
||||||
|
CI_PACKAGE: true
|
||||||
|
NODE_OPTIONS: "--openssl-legacy-provider"
|
||||||
|
run: yarn build
|
||||||
|
working-directory: ./element-web
|
||||||
|
- name: Edit Test Config
|
||||||
|
run: |
|
||||||
|
sed -i '/HOMESERVER/c\ HOMESERVER: "dendritePinecone",' cypress.config.ts
|
||||||
|
- name: "Run cypress tests"
|
||||||
|
uses: cypress-io/github-action@v4.1.1
|
||||||
|
with:
|
||||||
|
browser: chrome
|
||||||
|
start: npx serve -p 8080 ./element-web/webapp
|
||||||
|
wait-on: 'http://localhost:8080'
|
||||||
|
env:
|
||||||
|
PUPPETEER_SKIP_CHROMIUM_DOWNLOAD: true
|
||||||
|
TMPDIR: ${{ runner.temp }}
|
||||||
|
|
8
.gitignore
vendored
8
.gitignore
vendored
|
@ -5,6 +5,7 @@
|
||||||
|
|
||||||
# Allow GitHub config
|
# Allow GitHub config
|
||||||
!.github
|
!.github
|
||||||
|
!.forgejo
|
||||||
|
|
||||||
# Downloads
|
# Downloads
|
||||||
/.downloads
|
/.downloads
|
||||||
|
@ -74,3 +75,10 @@ complement/
|
||||||
docs/_site
|
docs/_site
|
||||||
|
|
||||||
media_store/
|
media_store/
|
||||||
|
build
|
||||||
|
|
||||||
|
# golang workspaces
|
||||||
|
go.work*
|
||||||
|
|
||||||
|
# helm chart
|
||||||
|
helm/dendrite/charts/
|
||||||
|
|
|
@ -6,7 +6,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: 30m
|
timeout: 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
|
||||||
|
@ -18,24 +18,6 @@ run:
|
||||||
#build-tags:
|
#build-tags:
|
||||||
# - mytag
|
# - mytag
|
||||||
|
|
||||||
# which dirs to skip: they won't be analyzed;
|
|
||||||
# can use regexp here: generated.*, regexp is applied on full path;
|
|
||||||
# default value is empty list, but next dirs are always skipped independently
|
|
||||||
# from this option's value:
|
|
||||||
# vendor$, third_party$, testdata$, examples$, Godeps$, builtin$
|
|
||||||
skip-dirs:
|
|
||||||
- bin
|
|
||||||
- docs
|
|
||||||
|
|
||||||
# which files to skip: they will be analyzed, but issues from them
|
|
||||||
# won't be reported. Default value is empty list, but there is
|
|
||||||
# no need to include all autogenerated files, we confidently recognize
|
|
||||||
# autogenerated files. If it's not please let us know.
|
|
||||||
skip-files:
|
|
||||||
- ".*\\.md$"
|
|
||||||
- ".*\\.sh$"
|
|
||||||
- "^cmd/syncserver-integration-tests/testdata.go$"
|
|
||||||
|
|
||||||
# by default isn't set. If set we pass it to "go list -mod={option}". From "go help modules":
|
# by default isn't set. If set we pass it to "go list -mod={option}". From "go help modules":
|
||||||
# If invoked with -mod=readonly, the go command is disallowed from the implicit
|
# If invoked with -mod=readonly, the go command is disallowed from the implicit
|
||||||
# automatic updating of go.mod described above. Instead, it fails when any changes
|
# automatic updating of go.mod described above. Instead, it fails when any changes
|
||||||
|
@ -50,7 +32,8 @@ run:
|
||||||
# output configuration options
|
# output configuration options
|
||||||
output:
|
output:
|
||||||
# colored-line-number|line-number|json|tab|checkstyle|code-climate, default is "colored-line-number"
|
# colored-line-number|line-number|json|tab|checkstyle|code-climate, default is "colored-line-number"
|
||||||
format: colored-line-number
|
formats:
|
||||||
|
- format: colored-line-number
|
||||||
|
|
||||||
# print lines of code with issue, default is true
|
# print lines of code with issue, default is true
|
||||||
print-issued-lines: true
|
print-issued-lines: true
|
||||||
|
@ -79,9 +62,8 @@ linters-settings:
|
||||||
# see https://github.com/kisielk/errcheck#excluding-functions for details
|
# see https://github.com/kisielk/errcheck#excluding-functions for details
|
||||||
#exclude: /path/to/file.txt
|
#exclude: /path/to/file.txt
|
||||||
govet:
|
govet:
|
||||||
# report about shadowed variables
|
enable:
|
||||||
check-shadowing: true
|
- shadow
|
||||||
|
|
||||||
# settings per analyzer
|
# settings per analyzer
|
||||||
settings:
|
settings:
|
||||||
printf: # analyzer name, run `go tool vet help` to see all analyzers
|
printf: # analyzer name, run `go tool vet help` to see all analyzers
|
||||||
|
@ -179,9 +161,7 @@ linters-settings:
|
||||||
|
|
||||||
linters:
|
linters:
|
||||||
enable:
|
enable:
|
||||||
- deadcode
|
|
||||||
- errcheck
|
- errcheck
|
||||||
- goconst
|
|
||||||
- gocyclo
|
- gocyclo
|
||||||
- goimports # Does everything gofmt does
|
- goimports # Does everything gofmt does
|
||||||
- gosimple
|
- gosimple
|
||||||
|
@ -191,10 +171,8 @@ linters:
|
||||||
- misspell # Check code comments, whereas misspell in CI checks *.md files
|
- misspell # Check code comments, whereas misspell in CI checks *.md files
|
||||||
- nakedret
|
- nakedret
|
||||||
- staticcheck
|
- staticcheck
|
||||||
- structcheck
|
|
||||||
- unparam
|
- unparam
|
||||||
- unused
|
- unused
|
||||||
- varcheck
|
|
||||||
enable-all: false
|
enable-all: false
|
||||||
disable:
|
disable:
|
||||||
- bodyclose
|
- bodyclose
|
||||||
|
@ -214,12 +192,31 @@ linters:
|
||||||
- stylecheck
|
- stylecheck
|
||||||
- typecheck # Should turn back on soon
|
- typecheck # Should turn back on soon
|
||||||
- unconvert # Should turn back on soon
|
- unconvert # Should turn back on soon
|
||||||
|
- goconst # Slightly annoying, as it reports "issues" in SQL statements
|
||||||
disable-all: false
|
disable-all: false
|
||||||
presets:
|
presets:
|
||||||
fast: false
|
fast: false
|
||||||
|
|
||||||
|
|
||||||
issues:
|
issues:
|
||||||
|
# which files to skip: they will be analyzed, but issues from them
|
||||||
|
# won't be reported. Default value is empty list, but there is
|
||||||
|
# no need to include all autogenerated files, we confidently recognize
|
||||||
|
# autogenerated files. If it's not please let us know.
|
||||||
|
exclude-files:
|
||||||
|
- ".*\\.md$"
|
||||||
|
- ".*\\.sh$"
|
||||||
|
- "^cmd/syncserver-integration-tests/testdata.go$"
|
||||||
|
|
||||||
|
# which dirs to skip: they won't be analyzed;
|
||||||
|
# can use regexp here: generated.*, regexp is applied on full path;
|
||||||
|
# default value is empty list, but next dirs are always skipped independently
|
||||||
|
# from this option's value:
|
||||||
|
# vendor$, third_party$, testdata$, examples$, Godeps$, builtin$
|
||||||
|
exclude-dirs:
|
||||||
|
- bin
|
||||||
|
- docs
|
||||||
|
|
||||||
# List of regexps of issue texts to exclude, empty list by default.
|
# List of regexps of issue texts to exclude, empty list by default.
|
||||||
# But independently from this option we use default exclude patterns,
|
# But independently from this option we use default exclude patterns,
|
||||||
# it can be disabled by `exclude-use-default: false`. To list all
|
# it can be disabled by `exclude-use-default: false`. To list all
|
||||||
|
|
180
CHANGES.md
180
CHANGES.md
|
@ -1,5 +1,185 @@
|
||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
## Dendrite 0.13.7 (2024-04-09)
|
||||||
|
|
||||||
|
### Fixes
|
||||||
|
|
||||||
|
- Fixed an issue where the displayname/avatar of an invited user was replaced with the inviter's details
|
||||||
|
- Improved server startup performance by avoiding unnecessary room ACL queries
|
||||||
|
- This change reduces memory footprint as it caches ACL regex patterns once instead of for each room
|
||||||
|
- Unnecessary Relay related queries have been removed. **Note**: To use relays, you now need to explicitly enable them using the `federation_api.enable_relays` config
|
||||||
|
- Fixed space summaries over federation
|
||||||
|
- Improved usage of external NATS JetStream by reusing existing connections instead of opening new ones unnecessarily
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
- Modernized Appservices (contributed by [tulir](https://github.com/tulir))
|
||||||
|
- Added event reporting with Synapse Admin endpoints for querying them
|
||||||
|
- Updated dependencies
|
||||||
|
|
||||||
|
## Dendrite 0.13.6 (2024-01-26)
|
||||||
|
|
||||||
|
Upgrading to this version is **highly** recommended, as it contains several QoL improvements.
|
||||||
|
|
||||||
|
### Fixes
|
||||||
|
|
||||||
|
- Use `AckExplicitPolicy` for JetStream consumers, so messages don't pile up in NATS
|
||||||
|
- A rare panic when assigning a state key NID has been fixed
|
||||||
|
- A rare panic when checking powerlevels has been fixed
|
||||||
|
- Notary keys requests for all keys now work correctly
|
||||||
|
- Spec compliance:
|
||||||
|
- Return `M_INVALID_PARAM` when querying room aliases
|
||||||
|
- Handle empty `from` parameter when requesting `/messages`
|
||||||
|
- Add CORP headers on media endpoints
|
||||||
|
- Remove `aliases` from `/publicRooms` responses
|
||||||
|
- Allow `+` in MXIDs (Contributed by [RosstheRoss](https://github.com/RosstheRoss))
|
||||||
|
- Fixes membership transitions from `knock` to `join` in `knock_restricted` rooms
|
||||||
|
- Incremental syncs now batch querying events (Contributed by [recht](https://github.com/recht))
|
||||||
|
- Move `/joined_members` back to the clientAPI/roomserver, which should make bridges happier again
|
||||||
|
- Backfilling from other servers now only uses at max 100 events instead of potentially thousands
|
||||||
|
|
||||||
|
## Dendrite 0.13.5 (2023-12-12)
|
||||||
|
|
||||||
|
Upgrading to this version is **highly** recommended, as it fixes several long-standing bugs in
|
||||||
|
our CanonicalJSON implementation.
|
||||||
|
|
||||||
|
### Fixes
|
||||||
|
|
||||||
|
- Convert unicode escapes to lowercase (gomatrixserverlib)
|
||||||
|
- Fix canonical json utf-16 surrogate pair detection logic (gomatrixserverlib)
|
||||||
|
- Handle negative zero and exponential numbers in Canonical JSON verification (gomatrixserverlib)
|
||||||
|
- Avoid logging unnecessary messages when unable to fetch server keys if multiple fetchers are used (gomatrixserverlib)
|
||||||
|
- Issues around the device list updater have been fixed, which should ensure that there are always
|
||||||
|
workers available to process incoming device list updates.
|
||||||
|
- A panic in the `/hierarchy` endpoints used for spaces has been fixed (client-server and server-server API)
|
||||||
|
- Fixes around the way we handle database transactions (including a potential connection leak)
|
||||||
|
- ACLs are now updated when received as outliers
|
||||||
|
- A race condition, which could lead to bridges instantly leaving a room after joining it, between the SyncAPI and
|
||||||
|
Appservices has been fixed
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
- **Appservice login is now supported!**
|
||||||
|
- Users can now kick themselves (used by some bridges)
|
||||||
|
|
||||||
|
## Dendrite 0.13.4 (2023-10-25)
|
||||||
|
|
||||||
|
Upgrading to this version is **highly** recommended, as it fixes a long-standing bug in the state resolution
|
||||||
|
algorithm.
|
||||||
|
|
||||||
|
### Fixes:
|
||||||
|
|
||||||
|
- The "device list updater" now de-duplicates the servers to fetch devices from on startup. (This also
|
||||||
|
avoids spamming the logs when shutting down.)
|
||||||
|
- A bug in the state resolution algorithm has been fixed. This bug could result in users "being reset"
|
||||||
|
out of rooms and other missing state events due to calculating the wrong state.
|
||||||
|
- A bug when setting notifications from Element Android has been fixed by implementing MSC3987
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
- Updated dependencies
|
||||||
|
- Internal NATS Server has been updated from v2.9.19 to v2.9.23
|
||||||
|
|
||||||
|
## Dendrite 0.13.3 (2023-09-28)
|
||||||
|
|
||||||
|
### Fixes:
|
||||||
|
|
||||||
|
- The `user_id` query parameter when authenticating is now used correctly (contributed by [tulir](https://github.com/tulir))
|
||||||
|
- Invitations are now correctly pushed to devices
|
||||||
|
- A bug which could result in the corruption of `m.direct` account data has been fixed
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
- [Sliding Sync proxy](https://github.com/matrix-org/sliding-sync) can be configured in the `/.well-known/matrix/client` response
|
||||||
|
- Room version 11 is now supported
|
||||||
|
- Clients can request the `federation` `event_format` when creating filters
|
||||||
|
- Many under the hood improvements for [MSC4014: Pseudonymous Identities](https://github.com/matrix-org/matrix-spec-proposals/blob/kegan/pseudo-ids/proposals/4014-pseudonymous-identities.md)
|
||||||
|
|
||||||
|
### Other
|
||||||
|
|
||||||
|
- Dendrite now requires Go 1.20 if building from source
|
||||||
|
|
||||||
|
## Dendrite 0.13.2 (2023-08-23)
|
||||||
|
|
||||||
|
### Fixes:
|
||||||
|
|
||||||
|
- Migrations in SQLite are now prepared on the correct context (transaction or database)
|
||||||
|
- The `InputRoomEvent` stream now has a maximum age of 24h, which should help with slow start up times of NATS JetStream (contributed by [neilalexander](https://github.com/neilalexander))
|
||||||
|
- Event size checks are more in line with Synapse
|
||||||
|
- Requests to `/messages` have been optimized, possibly reducing database round trips
|
||||||
|
- Re-add the revision of Dendrite when building from source (Note: This only works if git is installed)
|
||||||
|
- Getting local members to notify has been optimized, which should significantly reduce memory allocation and cache usage
|
||||||
|
- When getting queried about user profiles, we now return HTTP404 if the user/profiles does not exist
|
||||||
|
- Background federated joins should now be fixed and not timeout after a short time
|
||||||
|
- Database connections are now correctly re-used
|
||||||
|
- Restored the old behavior of the `/purgeRoom` admin endpoint (does not evacuate the room before purging)
|
||||||
|
- Don't expose information about the system when trying to download files that don't exist
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
- Further improvements and fixes for [MSC4014: Pseudonymous Identities](https://github.com/matrix-org/matrix-spec-proposals/blob/kegan/pseudo-ids/proposals/4014-pseudonymous-identities.md)
|
||||||
|
- Lookup correct prev events in the sync API
|
||||||
|
- Populate `prev_sender` correctly in the sync API
|
||||||
|
- Event federation should work better
|
||||||
|
- Added new `dendrite_up` Prometheus metric, containing the version of Dendrite
|
||||||
|
- Space summaries ([MSC2946](https://github.com/matrix-org/matrix-spec-proposals/pull/2946)) have been moved from MSC to being natively supported
|
||||||
|
- For easier issue investigation, logs for application services now contain the application service ID (contributed by [maxberger](https://github.com/maxberger))
|
||||||
|
- The default room version to use when creating rooms can now be configured using `room_server.default_room_version`
|
||||||
|
|
||||||
|
## Dendrite 0.13.1 (2023-07-06)
|
||||||
|
|
||||||
|
This releases fixes a long-standing "off-by-one" error which could result in state resets. Upgrading to this version is **highly** recommended.
|
||||||
|
|
||||||
|
When deduplicating state events, we were checking if the event in question was already in a state snapshot. If it was in a previous state snapshot, we would
|
||||||
|
then remove it from the list of events to store. If this happened, we were, unfortunately, skipping the next event to check. This resulted in
|
||||||
|
events getting stored in state snapshots where they may not be needed. When we now compared two of those state snapshots, one of them
|
||||||
|
contained the skipped event, while the other didn't. This difference possibly shouldn't exist, resulting in unexpected state resets and explains
|
||||||
|
reports of missing state events as well.
|
||||||
|
|
||||||
|
Rooms where a state reset occurred earlier should, hopefully, reconcile over time.
|
||||||
|
|
||||||
|
### Fixes:
|
||||||
|
|
||||||
|
- A long-standing "off-by-one" error has been fixed, which could result in state resets
|
||||||
|
- Roomserver Prometheus Metrics are available again
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
- Updated dependencies
|
||||||
|
- Internal NATS Server has been updated from v2.9.15 to v2.9.19
|
||||||
|
|
||||||
|
## Dendrite 0.13.0 (2023-06-30)
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
- Results in responses to `/search` now highlight words more accurately and not only the search terms as before
|
||||||
|
- Support for connecting to appservices listening on unix sockets has been added (contributed by [cyberb](https://github.com/cyberb))
|
||||||
|
- Admin APIs for token authenticated registration have been added (contributed by [santhoshivan23](https://github.com/santhoshivan23))
|
||||||
|
- Initial support for [MSC4014: Pseudonymous Identities](https://github.com/matrix-org/matrix-spec-proposals/blob/kegan/pseudo-ids/proposals/4014-pseudonymous-identities.md)
|
||||||
|
- This is **highly experimental**, things like changing usernames/avatars, inviting users, upgrading rooms isn't working
|
||||||
|
|
||||||
|
### Fixes
|
||||||
|
|
||||||
|
- `m.upload.size` is now optional, finally allowing uploads with unlimited file size
|
||||||
|
- A bug while resolving server names has been fixed (contributed by [anton-molyboha](https://github.com/anton-molyboha))
|
||||||
|
- Application services should only receive one invitation instead of 2 (or worse), which could result in state resets previously
|
||||||
|
- Several admin endpoints are now using `POST` instead of `GET`
|
||||||
|
- `/delete_devices` now uses user-interactive authentication
|
||||||
|
- Several "membership" (e.g `/kick`, `/ban`) endpoints are using less heavy database queries to check if the user is allowed to perform this action
|
||||||
|
- `/3pid` endpoints are now available on `/v3` instead of the `/unstable` prefix
|
||||||
|
- Upgrading rooms ignores state events of other users, which could result in failed upgrades before
|
||||||
|
- Uploading key backups with a wrong version now returns `M_WRONG_ROOM_KEYS_VERSION`
|
||||||
|
- A potential state reset when joining the same room multiple times in short sequence has been fixed
|
||||||
|
- A bug where we returned the full event as `redacted_because` in redaction events has been fixed
|
||||||
|
- The `displayname` and `avatar_url` can now be set to empty strings
|
||||||
|
- Unsafe hotserving of files has been fixed (contributed by [joshqou](https://github.com/joshqou))
|
||||||
|
- Joining new rooms would potentially return "redacted" events, due to history visibility not being set correctly, this could result in events being rejected
|
||||||
|
- Backfilling resulting in `unsuported room version ''` should now be solved
|
||||||
|
|
||||||
|
### Other
|
||||||
|
|
||||||
|
- Huge refactoring of Dendrite and gomatrixserverlib
|
||||||
|
|
||||||
## Dendrite 0.12.0 (2023-03-13)
|
## Dendrite 0.12.0 (2023-03-13)
|
||||||
|
|
||||||
### Features
|
### Features
|
||||||
|
|
|
@ -3,8 +3,9 @@
|
||||||
#
|
#
|
||||||
# base installs required dependencies and runs go mod download to cache dependencies
|
# base installs required dependencies and runs go mod download to cache dependencies
|
||||||
#
|
#
|
||||||
FROM --platform=${BUILDPLATFORM} docker.io/golang:1.20-alpine AS base
|
# Pinned to alpine3.18 until https://github.com/mattn/go-sqlite3/issues/1164 is solved
|
||||||
RUN apk --update --no-cache add bash build-base curl
|
FROM --platform=${BUILDPLATFORM} docker.io/golang:1.21-alpine3.18 AS base
|
||||||
|
RUN apk --update --no-cache add bash build-base curl git
|
||||||
|
|
||||||
#
|
#
|
||||||
# build creates all needed binaries
|
# build creates all needed binaries
|
||||||
|
@ -13,7 +14,6 @@ FROM --platform=${BUILDPLATFORM} base AS build
|
||||||
WORKDIR /src
|
WORKDIR /src
|
||||||
ARG TARGETOS
|
ARG TARGETOS
|
||||||
ARG TARGETARCH
|
ARG TARGETARCH
|
||||||
ARG FLAGS
|
|
||||||
RUN --mount=target=. \
|
RUN --mount=target=. \
|
||||||
--mount=type=cache,target=/root/.cache/go-build \
|
--mount=type=cache,target=/root/.cache/go-build \
|
||||||
--mount=type=cache,target=/go/pkg/mod \
|
--mount=type=cache,target=/go/pkg/mod \
|
||||||
|
@ -21,7 +21,7 @@ RUN --mount=target=. \
|
||||||
GOARCH="$TARGETARCH" \
|
GOARCH="$TARGETARCH" \
|
||||||
GOOS="linux" \
|
GOOS="linux" \
|
||||||
CGO_ENABLED=$([ "$TARGETARCH" = "$USERARCH" ] && echo "1" || echo "0") \
|
CGO_ENABLED=$([ "$TARGETARCH" = "$USERARCH" ] && echo "1" || echo "0") \
|
||||||
go build -v -ldflags="${FLAGS}" -trimpath -o /out/ ./cmd/...
|
go build -v -trimpath -o /out/ ./cmd/...
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
|
|
12
README.md
12
README.md
|
@ -13,7 +13,7 @@ It intends to provide an **efficient**, **reliable** and **scalable** alternativ
|
||||||
|
|
||||||
Dendrite is **beta** software, which means:
|
Dendrite is **beta** software, which means:
|
||||||
|
|
||||||
- Dendrite is ready for early adopters. We recommend running in Monolith mode with a PostgreSQL database.
|
- Dendrite is ready for early adopters. We recommend running Dendrite with a PostgreSQL database.
|
||||||
- Dendrite has periodic releases. We intend to release new versions as we fix bugs and land significant features.
|
- Dendrite has periodic releases. We intend to release new versions as we fix bugs and land significant features.
|
||||||
- Dendrite supports database schema upgrades between releases. This means you should never lose your messages when upgrading Dendrite.
|
- Dendrite supports database schema upgrades between releases. This means you should never lose your messages when upgrading Dendrite.
|
||||||
|
|
||||||
|
@ -21,7 +21,7 @@ This does not mean:
|
||||||
|
|
||||||
- Dendrite is bug-free. It has not yet been battle-tested in the real world and so will be error prone initially.
|
- Dendrite is bug-free. It has not yet been battle-tested in the real world and so will be error prone initially.
|
||||||
- Dendrite is feature-complete. There may be client or federation APIs that are not implemented.
|
- Dendrite is feature-complete. There may be client or federation APIs that are not implemented.
|
||||||
- Dendrite is ready for massive homeserver deployments. There is no sharding of microservices (although it is possible to run them on separate machines) and there is no high-availability/clustering support.
|
- Dendrite is ready for massive homeserver deployments. There is no high-availability/clustering support.
|
||||||
|
|
||||||
Currently, we expect Dendrite to function well for small (10s/100s of users) homeserver deployments as well as P2P Matrix nodes in-browser or on mobile devices.
|
Currently, we expect Dendrite to function well for small (10s/100s of users) homeserver deployments as well as P2P Matrix nodes in-browser or on mobile devices.
|
||||||
|
|
||||||
|
@ -36,7 +36,7 @@ If you have further questions, please take a look at [our FAQ](docs/FAQ.md) or j
|
||||||
See the [Planning your Installation](https://matrix-org.github.io/dendrite/installation/planning) page for
|
See the [Planning your Installation](https://matrix-org.github.io/dendrite/installation/planning) page for
|
||||||
more information on requirements.
|
more information on requirements.
|
||||||
|
|
||||||
To build Dendrite, you will need Go 1.18 or later.
|
To build Dendrite, you will need Go 1.20 or later.
|
||||||
|
|
||||||
For a usable federating Dendrite deployment, you will also need:
|
For a usable federating Dendrite deployment, you will also need:
|
||||||
|
|
||||||
|
@ -47,7 +47,7 @@ For a usable federating Dendrite deployment, you will also need:
|
||||||
Also recommended are:
|
Also recommended are:
|
||||||
|
|
||||||
- A PostgreSQL database engine, which will perform better than SQLite with many users and/or larger rooms
|
- A PostgreSQL database engine, which will perform better than SQLite with many users and/or larger rooms
|
||||||
- A reverse proxy server, such as nginx, configured [like this sample](https://github.com/matrix-org/dendrite/blob/master/docs/nginx/monolith-sample.conf)
|
- A reverse proxy server, such as nginx, configured [like this sample](https://github.com/matrix-org/dendrite/blob/main/docs/nginx/dendrite-sample.conf)
|
||||||
|
|
||||||
The [Federation Tester](https://federationtester.matrix.org) can be used to verify your deployment.
|
The [Federation Tester](https://federationtester.matrix.org) can be used to verify your deployment.
|
||||||
|
|
||||||
|
@ -60,7 +60,7 @@ The following instructions are enough to get Dendrite started as a non-federatin
|
||||||
```bash
|
```bash
|
||||||
$ git clone https://github.com/matrix-org/dendrite
|
$ git clone https://github.com/matrix-org/dendrite
|
||||||
$ cd dendrite
|
$ cd dendrite
|
||||||
$ ./build.sh
|
$ go build -o bin/ ./cmd/...
|
||||||
|
|
||||||
# Generate a Matrix signing key for federation (required)
|
# Generate a Matrix signing key for federation (required)
|
||||||
$ ./bin/generate-keys --private-key matrix_key.pem
|
$ ./bin/generate-keys --private-key matrix_key.pem
|
||||||
|
@ -85,7 +85,7 @@ Then point your favourite Matrix client at `http://localhost:8008` or `https://l
|
||||||
|
|
||||||
## Progress
|
## Progress
|
||||||
|
|
||||||
We use a script called Are We Synapse Yet which checks Sytest compliance rates. Sytest is a black-box homeserver
|
We use a script called "Are We Synapse Yet" which checks Sytest compliance rates. Sytest is a black-box homeserver
|
||||||
test rig with around 900 tests. The script works out how many of these tests are passing on Dendrite and it
|
test rig with around 900 tests. The script works out how many of these tests are passing on Dendrite and it
|
||||||
updates with CI. As of January 2023, we have 100% server-server parity with Synapse, and the client-server parity is at 93% , though check
|
updates with CI. As of January 2023, we have 100% server-server parity with Synapse, and the client-server parity is at 93% , though check
|
||||||
CI for the latest numbers. In practice, this means you can communicate locally and via federation with Synapse
|
CI for the latest numbers. In practice, this means you can communicate locally and via federation with Synapse
|
||||||
|
|
|
@ -22,8 +22,6 @@ import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
|
|
||||||
"github.com/matrix-org/gomatrixserverlib"
|
|
||||||
|
|
||||||
"github.com/matrix-org/dendrite/clientapi/auth/authtypes"
|
"github.com/matrix-org/dendrite/clientapi/auth/authtypes"
|
||||||
userapi "github.com/matrix-org/dendrite/userapi/api"
|
userapi "github.com/matrix-org/dendrite/userapi/api"
|
||||||
)
|
)
|
||||||
|
@ -84,9 +82,17 @@ type UserIDExistsResponse struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
const (
|
const (
|
||||||
ASProtocolPath = "/_matrix/app/unstable/thirdparty/protocol/"
|
ASProtocolLegacyPath = "/_matrix/app/unstable/thirdparty/protocol/"
|
||||||
ASUserPath = "/_matrix/app/unstable/thirdparty/user"
|
ASUserLegacyPath = "/_matrix/app/unstable/thirdparty/user"
|
||||||
ASLocationPath = "/_matrix/app/unstable/thirdparty/location"
|
ASLocationLegacyPath = "/_matrix/app/unstable/thirdparty/location"
|
||||||
|
ASRoomAliasExistsLegacyPath = "/rooms/"
|
||||||
|
ASUserExistsLegacyPath = "/users/"
|
||||||
|
|
||||||
|
ASProtocolPath = "/_matrix/app/v1/thirdparty/protocol/"
|
||||||
|
ASUserPath = "/_matrix/app/v1/thirdparty/user"
|
||||||
|
ASLocationPath = "/_matrix/app/v1/thirdparty/location"
|
||||||
|
ASRoomAliasExistsPath = "/_matrix/app/v1/rooms/"
|
||||||
|
ASUserExistsPath = "/_matrix/app/v1/users/"
|
||||||
)
|
)
|
||||||
|
|
||||||
type ProtocolRequest struct {
|
type ProtocolRequest struct {
|
||||||
|
@ -150,6 +156,10 @@ type ASLocationResponse struct {
|
||||||
Fields json.RawMessage `json:"fields"`
|
Fields json.RawMessage `json:"fields"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ErrProfileNotExists is returned when trying to lookup a user's profile that
|
||||||
|
// doesn't exist locally.
|
||||||
|
var ErrProfileNotExists = errors.New("no known profile for given user ID")
|
||||||
|
|
||||||
// RetrieveUserProfile is a wrapper that queries both the local database and
|
// RetrieveUserProfile is a wrapper that queries both the local database and
|
||||||
// application services for a given user's profile
|
// application services for a given user's profile
|
||||||
// TODO: Remove this, it's called from federationapi and clientapi but is a pure function
|
// TODO: Remove this, it's called from federationapi and clientapi but is a pure function
|
||||||
|
@ -157,25 +167,11 @@ func RetrieveUserProfile(
|
||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
userID string,
|
userID string,
|
||||||
asAPI AppServiceInternalAPI,
|
asAPI AppServiceInternalAPI,
|
||||||
profileAPI userapi.ClientUserAPI,
|
profileAPI userapi.ProfileAPI,
|
||||||
) (*authtypes.Profile, error) {
|
) (*authtypes.Profile, error) {
|
||||||
localpart, _, err := gomatrixserverlib.SplitID('@', userID)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// Try to query the user from the local database
|
// Try to query the user from the local database
|
||||||
res := &userapi.QueryProfileResponse{}
|
profile, err := profileAPI.QueryProfile(ctx, userID)
|
||||||
err = profileAPI.QueryProfile(ctx, &userapi.QueryProfileRequest{UserID: userID}, res)
|
if err == nil {
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
profile := &authtypes.Profile{
|
|
||||||
Localpart: localpart,
|
|
||||||
DisplayName: res.DisplayName,
|
|
||||||
AvatarURL: res.AvatarURL,
|
|
||||||
}
|
|
||||||
if res.UserExists {
|
|
||||||
return profile, nil
|
return profile, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -188,19 +184,15 @@ func RetrieveUserProfile(
|
||||||
|
|
||||||
// If no user exists, return
|
// If no user exists, return
|
||||||
if !userResp.UserIDExists {
|
if !userResp.UserIDExists {
|
||||||
return nil, errors.New("no known profile for given user ID")
|
return nil, ErrProfileNotExists
|
||||||
}
|
}
|
||||||
|
|
||||||
// Try to query the user from the local database again
|
// Try to query the user from the local database again
|
||||||
err = profileAPI.QueryProfile(ctx, &userapi.QueryProfileRequest{UserID: userID}, res)
|
profile, err = profileAPI.QueryProfile(ctx, userID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// profile should not be nil at this point
|
// profile should not be nil at this point
|
||||||
return &authtypes.Profile{
|
return profile, nil
|
||||||
Localpart: localpart,
|
|
||||||
DisplayName: res.DisplayName,
|
|
||||||
AvatarURL: res.AvatarURL,
|
|
||||||
}, nil
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -16,20 +16,17 @@ package appservice
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"crypto/tls"
|
|
||||||
"net/http"
|
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
|
||||||
|
|
||||||
|
"github.com/matrix-org/dendrite/setup/jetstream"
|
||||||
|
"github.com/matrix-org/dendrite/setup/process"
|
||||||
|
"github.com/matrix-org/gomatrixserverlib/spec"
|
||||||
"github.com/sirupsen/logrus"
|
"github.com/sirupsen/logrus"
|
||||||
|
|
||||||
"github.com/matrix-org/gomatrixserverlib"
|
|
||||||
|
|
||||||
appserviceAPI "github.com/matrix-org/dendrite/appservice/api"
|
appserviceAPI "github.com/matrix-org/dendrite/appservice/api"
|
||||||
"github.com/matrix-org/dendrite/appservice/consumers"
|
"github.com/matrix-org/dendrite/appservice/consumers"
|
||||||
"github.com/matrix-org/dendrite/appservice/query"
|
"github.com/matrix-org/dendrite/appservice/query"
|
||||||
roomserverAPI "github.com/matrix-org/dendrite/roomserver/api"
|
roomserverAPI "github.com/matrix-org/dendrite/roomserver/api"
|
||||||
"github.com/matrix-org/dendrite/setup/base"
|
|
||||||
"github.com/matrix-org/dendrite/setup/config"
|
"github.com/matrix-org/dendrite/setup/config"
|
||||||
userapi "github.com/matrix-org/dendrite/userapi/api"
|
userapi "github.com/matrix-org/dendrite/userapi/api"
|
||||||
)
|
)
|
||||||
|
@ -37,39 +34,31 @@ import (
|
||||||
// NewInternalAPI returns a concerete implementation of the internal API. Callers
|
// NewInternalAPI returns a concerete implementation of the internal API. Callers
|
||||||
// can call functions directly on the returned API or via an HTTP interface using AddInternalRoutes.
|
// can call functions directly on the returned API or via an HTTP interface using AddInternalRoutes.
|
||||||
func NewInternalAPI(
|
func NewInternalAPI(
|
||||||
base *base.BaseDendrite,
|
processContext *process.ProcessContext,
|
||||||
|
cfg *config.Dendrite,
|
||||||
|
natsInstance *jetstream.NATSInstance,
|
||||||
userAPI userapi.AppserviceUserAPI,
|
userAPI userapi.AppserviceUserAPI,
|
||||||
rsAPI roomserverAPI.RoomserverInternalAPI,
|
rsAPI roomserverAPI.RoomserverInternalAPI,
|
||||||
) appserviceAPI.AppServiceInternalAPI {
|
) appserviceAPI.AppServiceInternalAPI {
|
||||||
client := &http.Client{
|
|
||||||
Timeout: time.Second * 30,
|
|
||||||
Transport: &http.Transport{
|
|
||||||
DisableKeepAlives: true,
|
|
||||||
TLSClientConfig: &tls.Config{
|
|
||||||
InsecureSkipVerify: base.Cfg.AppServiceAPI.DisableTLSValidation,
|
|
||||||
},
|
|
||||||
Proxy: http.ProxyFromEnvironment,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
// Create appserivce query API with an HTTP client that will be used for all
|
// Create appserivce query API with an HTTP client that will be used for all
|
||||||
// outbound and inbound requests (inbound only for the internal API)
|
// outbound and inbound requests (inbound only for the internal API)
|
||||||
appserviceQueryAPI := &query.AppServiceQueryAPI{
|
appserviceQueryAPI := &query.AppServiceQueryAPI{
|
||||||
HTTPClient: client,
|
Cfg: &cfg.AppServiceAPI,
|
||||||
Cfg: &base.Cfg.AppServiceAPI,
|
|
||||||
ProtocolCache: map[string]appserviceAPI.ASProtocolResponse{},
|
ProtocolCache: map[string]appserviceAPI.ASProtocolResponse{},
|
||||||
CacheMu: sync.Mutex{},
|
CacheMu: sync.Mutex{},
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(base.Cfg.Derived.ApplicationServices) == 0 {
|
if len(cfg.Derived.ApplicationServices) == 0 {
|
||||||
return appserviceQueryAPI
|
return appserviceQueryAPI
|
||||||
}
|
}
|
||||||
|
|
||||||
// Wrap application services in a type that relates the application service and
|
// Wrap application services in a type that relates the application service and
|
||||||
// a sync.Cond object that can be used to notify workers when there are new
|
// a sync.Cond object that can be used to notify workers when there are new
|
||||||
// events to be sent out.
|
// events to be sent out.
|
||||||
for _, appservice := range base.Cfg.Derived.ApplicationServices {
|
for _, appservice := range cfg.Derived.ApplicationServices {
|
||||||
// Create bot account for this AS if it doesn't already exist
|
// Create bot account for this AS if it doesn't already exist
|
||||||
if err := generateAppServiceAccount(userAPI, appservice, base.Cfg.Global.ServerName); err != nil {
|
if err := generateAppServiceAccount(userAPI, appservice, cfg.Global.ServerName); err != nil {
|
||||||
logrus.WithFields(logrus.Fields{
|
logrus.WithFields(logrus.Fields{
|
||||||
"appservice": appservice.ID,
|
"appservice": appservice.ID,
|
||||||
}).WithError(err).Panicf("failed to generate bot account for appservice")
|
}).WithError(err).Panicf("failed to generate bot account for appservice")
|
||||||
|
@ -78,10 +67,10 @@ func NewInternalAPI(
|
||||||
|
|
||||||
// Only consume if we actually have ASes to track, else we'll just chew cycles needlessly.
|
// Only consume if we actually have ASes to track, else we'll just chew cycles needlessly.
|
||||||
// We can't add ASes at runtime so this is safe to do.
|
// We can't add ASes at runtime so this is safe to do.
|
||||||
js, _ := base.NATS.Prepare(base.ProcessContext, &base.Cfg.Global.JetStream)
|
js, _ := natsInstance.Prepare(processContext, &cfg.Global.JetStream)
|
||||||
consumer := consumers.NewOutputRoomEventConsumer(
|
consumer := consumers.NewOutputRoomEventConsumer(
|
||||||
base.ProcessContext, &base.Cfg.AppServiceAPI,
|
processContext, &cfg.AppServiceAPI,
|
||||||
client, js, rsAPI,
|
js, rsAPI,
|
||||||
)
|
)
|
||||||
if err := consumer.Start(); err != nil {
|
if err := consumer.Start(); err != nil {
|
||||||
logrus.WithError(err).Panicf("failed to start appservice roomserver consumer")
|
logrus.WithError(err).Panicf("failed to start appservice roomserver consumer")
|
||||||
|
@ -96,7 +85,7 @@ func NewInternalAPI(
|
||||||
func generateAppServiceAccount(
|
func generateAppServiceAccount(
|
||||||
userAPI userapi.AppserviceUserAPI,
|
userAPI userapi.AppserviceUserAPI,
|
||||||
as config.ApplicationService,
|
as config.ApplicationService,
|
||||||
serverName gomatrixserverlib.ServerName,
|
serverName spec.ServerName,
|
||||||
) error {
|
) error {
|
||||||
var accRes userapi.PerformAccountCreationResponse
|
var accRes userapi.PerformAccountCreationResponse
|
||||||
err := userAPI.PerformAccountCreation(context.Background(), &userapi.PerformAccountCreationRequest{
|
err := userAPI.PerformAccountCreation(context.Background(), &userapi.PerformAccountCreationRequest{
|
||||||
|
|
|
@ -3,23 +3,50 @@ package appservice_test
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/http/httptest"
|
"net/http/httptest"
|
||||||
|
"path"
|
||||||
"reflect"
|
"reflect"
|
||||||
"regexp"
|
"regexp"
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/matrix-org/dendrite/clientapi"
|
||||||
|
"github.com/matrix-org/dendrite/clientapi/auth/authtypes"
|
||||||
|
"github.com/matrix-org/dendrite/federationapi/statistics"
|
||||||
|
"github.com/matrix-org/dendrite/internal/httputil"
|
||||||
|
"github.com/matrix-org/dendrite/roomserver/types"
|
||||||
|
"github.com/matrix-org/dendrite/syncapi"
|
||||||
|
uapi "github.com/matrix-org/dendrite/userapi/api"
|
||||||
|
"github.com/matrix-org/gomatrixserverlib"
|
||||||
|
"github.com/matrix-org/util"
|
||||||
|
"github.com/nats-io/nats.go"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/tidwall/gjson"
|
||||||
|
|
||||||
"github.com/matrix-org/dendrite/appservice"
|
"github.com/matrix-org/dendrite/appservice"
|
||||||
"github.com/matrix-org/dendrite/appservice/api"
|
"github.com/matrix-org/dendrite/appservice/api"
|
||||||
|
"github.com/matrix-org/dendrite/appservice/consumers"
|
||||||
|
"github.com/matrix-org/dendrite/internal/caching"
|
||||||
|
"github.com/matrix-org/dendrite/internal/sqlutil"
|
||||||
"github.com/matrix-org/dendrite/roomserver"
|
"github.com/matrix-org/dendrite/roomserver"
|
||||||
|
rsapi "github.com/matrix-org/dendrite/roomserver/api"
|
||||||
"github.com/matrix-org/dendrite/setup/config"
|
"github.com/matrix-org/dendrite/setup/config"
|
||||||
|
"github.com/matrix-org/dendrite/setup/jetstream"
|
||||||
"github.com/matrix-org/dendrite/test"
|
"github.com/matrix-org/dendrite/test"
|
||||||
"github.com/matrix-org/dendrite/userapi"
|
"github.com/matrix-org/dendrite/userapi"
|
||||||
|
"github.com/matrix-org/gomatrixserverlib/spec"
|
||||||
|
|
||||||
"github.com/matrix-org/dendrite/test/testrig"
|
"github.com/matrix-org/dendrite/test/testrig"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
var testIsBlacklistedOrBackingOff = func(s spec.ServerName) (*statistics.ServerStatistics, error) {
|
||||||
|
return &statistics.ServerStatistics{}, nil
|
||||||
|
}
|
||||||
|
|
||||||
func TestAppserviceInternalAPI(t *testing.T) {
|
func TestAppserviceInternalAPI(t *testing.T) {
|
||||||
|
|
||||||
// Set expected results
|
// Set expected results
|
||||||
|
@ -104,34 +131,139 @@ func TestAppserviceInternalAPI(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
test.WithAllDatabases(t, func(t *testing.T, dbType test.DBType) {
|
test.WithAllDatabases(t, func(t *testing.T, dbType test.DBType) {
|
||||||
base, closeBase := testrig.CreateBaseDendrite(t, dbType)
|
cfg, ctx, close := testrig.CreateConfig(t, dbType)
|
||||||
defer closeBase()
|
defer close()
|
||||||
|
|
||||||
// Create a dummy application service
|
// Create a dummy application service
|
||||||
base.Cfg.AppServiceAPI.Derived.ApplicationServices = []config.ApplicationService{
|
as := &config.ApplicationService{
|
||||||
{
|
ID: "someID",
|
||||||
ID: "someID",
|
URL: srv.URL,
|
||||||
URL: srv.URL,
|
ASToken: "",
|
||||||
ASToken: "",
|
HSToken: "",
|
||||||
HSToken: "",
|
SenderLocalpart: "senderLocalPart",
|
||||||
SenderLocalpart: "senderLocalPart",
|
NamespaceMap: map[string][]config.ApplicationServiceNamespace{
|
||||||
NamespaceMap: map[string][]config.ApplicationServiceNamespace{
|
"users": {{RegexpObject: regexp.MustCompile("as-.*")}},
|
||||||
"users": {{RegexpObject: regexp.MustCompile("as-.*")}},
|
"aliases": {{RegexpObject: regexp.MustCompile("asroom-.*")}},
|
||||||
"aliases": {{RegexpObject: regexp.MustCompile("asroom-.*")}},
|
|
||||||
},
|
|
||||||
Protocols: []string{existingProtocol},
|
|
||||||
},
|
},
|
||||||
|
Protocols: []string{existingProtocol},
|
||||||
}
|
}
|
||||||
|
as.CreateHTTPClient(cfg.AppServiceAPI.DisableTLSValidation)
|
||||||
|
cfg.AppServiceAPI.Derived.ApplicationServices = []config.ApplicationService{*as}
|
||||||
|
t.Cleanup(func() {
|
||||||
|
ctx.ShutdownDendrite()
|
||||||
|
ctx.WaitForShutdown()
|
||||||
|
})
|
||||||
|
caches := caching.NewRistrettoCache(128*1024*1024, time.Hour, caching.DisableMetrics)
|
||||||
// Create required internal APIs
|
// Create required internal APIs
|
||||||
rsAPI := roomserver.NewInternalAPI(base)
|
natsInstance := jetstream.NATSInstance{}
|
||||||
usrAPI := userapi.NewInternalAPI(base, rsAPI, nil)
|
cm := sqlutil.NewConnectionManager(ctx, cfg.Global.DatabaseOptions)
|
||||||
asAPI := appservice.NewInternalAPI(base, usrAPI, rsAPI)
|
rsAPI := roomserver.NewInternalAPI(ctx, cfg, cm, &natsInstance, caches, caching.DisableMetrics)
|
||||||
|
rsAPI.SetFederationAPI(nil, nil)
|
||||||
|
usrAPI := userapi.NewInternalAPI(ctx, cfg, cm, &natsInstance, rsAPI, nil, caching.DisableMetrics, testIsBlacklistedOrBackingOff)
|
||||||
|
asAPI := appservice.NewInternalAPI(ctx, cfg, &natsInstance, usrAPI, rsAPI)
|
||||||
|
|
||||||
runCases(t, asAPI)
|
runCases(t, asAPI)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestAppserviceInternalAPI_UnixSocket_Simple(t *testing.T) {
|
||||||
|
|
||||||
|
// Set expected results
|
||||||
|
existingProtocol := "irc"
|
||||||
|
wantLocationResponse := []api.ASLocationResponse{{Protocol: existingProtocol, Fields: []byte("{}")}}
|
||||||
|
wantUserResponse := []api.ASUserResponse{{Protocol: existingProtocol, Fields: []byte("{}")}}
|
||||||
|
wantProtocolResponse := api.ASProtocolResponse{Instances: []api.ProtocolInstance{{Fields: []byte("{}")}}}
|
||||||
|
|
||||||
|
// create a dummy AS url, handling some cases
|
||||||
|
srv := httptest.NewUnstartedServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
switch {
|
||||||
|
case strings.Contains(r.URL.Path, "location"):
|
||||||
|
// Check if we've got an existing protocol, if so, return a proper response.
|
||||||
|
if r.URL.Path[len(r.URL.Path)-len(existingProtocol):] == existingProtocol {
|
||||||
|
if err := json.NewEncoder(w).Encode(wantLocationResponse); err != nil {
|
||||||
|
t.Fatalf("failed to encode response: %s", err)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err := json.NewEncoder(w).Encode([]api.ASLocationResponse{}); err != nil {
|
||||||
|
t.Fatalf("failed to encode response: %s", err)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
case strings.Contains(r.URL.Path, "user"):
|
||||||
|
if r.URL.Path[len(r.URL.Path)-len(existingProtocol):] == existingProtocol {
|
||||||
|
if err := json.NewEncoder(w).Encode(wantUserResponse); err != nil {
|
||||||
|
t.Fatalf("failed to encode response: %s", err)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err := json.NewEncoder(w).Encode([]api.UserResponse{}); err != nil {
|
||||||
|
t.Fatalf("failed to encode response: %s", err)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
case strings.Contains(r.URL.Path, "protocol"):
|
||||||
|
if r.URL.Path[len(r.URL.Path)-len(existingProtocol):] == existingProtocol {
|
||||||
|
if err := json.NewEncoder(w).Encode(wantProtocolResponse); err != nil {
|
||||||
|
t.Fatalf("failed to encode response: %s", err)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err := json.NewEncoder(w).Encode(nil); err != nil {
|
||||||
|
t.Fatalf("failed to encode response: %s", err)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
default:
|
||||||
|
t.Logf("hit location: %s", r.URL.Path)
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
|
||||||
|
tmpDir := t.TempDir()
|
||||||
|
socket := path.Join(tmpDir, "socket")
|
||||||
|
l, err := net.Listen("unix", socket)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
_ = srv.Listener.Close()
|
||||||
|
srv.Listener = l
|
||||||
|
srv.Start()
|
||||||
|
defer srv.Close()
|
||||||
|
|
||||||
|
cfg, ctx, tearDown := testrig.CreateConfig(t, test.DBTypeSQLite)
|
||||||
|
defer tearDown()
|
||||||
|
|
||||||
|
// Create a dummy application service
|
||||||
|
as := &config.ApplicationService{
|
||||||
|
ID: "someID",
|
||||||
|
URL: fmt.Sprintf("unix://%s", socket),
|
||||||
|
ASToken: "",
|
||||||
|
HSToken: "",
|
||||||
|
SenderLocalpart: "senderLocalPart",
|
||||||
|
NamespaceMap: map[string][]config.ApplicationServiceNamespace{
|
||||||
|
"users": {{RegexpObject: regexp.MustCompile("as-.*")}},
|
||||||
|
"aliases": {{RegexpObject: regexp.MustCompile("asroom-.*")}},
|
||||||
|
},
|
||||||
|
Protocols: []string{existingProtocol},
|
||||||
|
}
|
||||||
|
as.CreateHTTPClient(cfg.AppServiceAPI.DisableTLSValidation)
|
||||||
|
cfg.AppServiceAPI.Derived.ApplicationServices = []config.ApplicationService{*as}
|
||||||
|
|
||||||
|
t.Cleanup(func() {
|
||||||
|
ctx.ShutdownDendrite()
|
||||||
|
ctx.WaitForShutdown()
|
||||||
|
})
|
||||||
|
caches := caching.NewRistrettoCache(128*1024*1024, time.Hour, caching.DisableMetrics)
|
||||||
|
// Create required internal APIs
|
||||||
|
natsInstance := jetstream.NATSInstance{}
|
||||||
|
cm := sqlutil.NewConnectionManager(ctx, cfg.Global.DatabaseOptions)
|
||||||
|
rsAPI := roomserver.NewInternalAPI(ctx, cfg, cm, &natsInstance, caches, caching.DisableMetrics)
|
||||||
|
rsAPI.SetFederationAPI(nil, nil)
|
||||||
|
usrAPI := userapi.NewInternalAPI(ctx, cfg, cm, &natsInstance, rsAPI, nil, caching.DisableMetrics, testIsBlacklistedOrBackingOff)
|
||||||
|
asAPI := appservice.NewInternalAPI(ctx, cfg, &natsInstance, usrAPI, rsAPI)
|
||||||
|
|
||||||
|
t.Run("UserIDExists", func(t *testing.T) {
|
||||||
|
testUserIDExists(t, asAPI, "@as-testing:test", true)
|
||||||
|
testUserIDExists(t, asAPI, "@as1-testing:test", false)
|
||||||
|
})
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
func testUserIDExists(t *testing.T, asAPI api.AppServiceInternalAPI, userID string, wantExists bool) {
|
func testUserIDExists(t *testing.T, asAPI api.AppServiceInternalAPI, userID string, wantExists bool) {
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
userResp := &api.UserIDExistsResponse{}
|
userResp := &api.UserIDExistsResponse{}
|
||||||
|
@ -201,3 +333,274 @@ func testProtocol(t *testing.T, asAPI api.AppServiceInternalAPI, proto string, w
|
||||||
t.Errorf("unexpected result for Protocols(%s): %+v, expected %+v", proto, protoResp.Protocols[proto], wantResult)
|
t.Errorf("unexpected result for Protocols(%s): %+v, expected %+v", proto, protoResp.Protocols[proto], wantResult)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Tests that the roomserver consumer only receives one invite
|
||||||
|
func TestRoomserverConsumerOneInvite(t *testing.T) {
|
||||||
|
|
||||||
|
alice := test.NewUser(t)
|
||||||
|
bob := test.NewUser(t)
|
||||||
|
room := test.NewRoom(t, alice)
|
||||||
|
|
||||||
|
// Invite Bob
|
||||||
|
room.CreateAndInsert(t, alice, spec.MRoomMember, map[string]interface{}{
|
||||||
|
"membership": "invite",
|
||||||
|
}, test.WithStateKey(bob.ID))
|
||||||
|
|
||||||
|
test.WithAllDatabases(t, func(t *testing.T, dbType test.DBType) {
|
||||||
|
cfg, processCtx, closeDB := testrig.CreateConfig(t, dbType)
|
||||||
|
defer closeDB()
|
||||||
|
cm := sqlutil.NewConnectionManager(processCtx, cfg.Global.DatabaseOptions)
|
||||||
|
natsInstance := &jetstream.NATSInstance{}
|
||||||
|
|
||||||
|
evChan := make(chan struct{})
|
||||||
|
// create a dummy AS url, handling the events
|
||||||
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
var txn consumers.ApplicationServiceTransaction
|
||||||
|
err := json.NewDecoder(r.Body).Decode(&txn)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
for _, ev := range txn.Events {
|
||||||
|
if ev.Type != spec.MRoomMember {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
// Usually we would check the event content for the membership, but since
|
||||||
|
// we only invited bob, this should be fine for this test.
|
||||||
|
if ev.StateKey != nil && *ev.StateKey == bob.ID {
|
||||||
|
evChan <- struct{}{}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
defer srv.Close()
|
||||||
|
|
||||||
|
as := &config.ApplicationService{
|
||||||
|
ID: "someID",
|
||||||
|
URL: srv.URL,
|
||||||
|
ASToken: "",
|
||||||
|
HSToken: "",
|
||||||
|
SenderLocalpart: "senderLocalPart",
|
||||||
|
NamespaceMap: map[string][]config.ApplicationServiceNamespace{
|
||||||
|
"users": {{RegexpObject: regexp.MustCompile(bob.ID)}},
|
||||||
|
"aliases": {{RegexpObject: regexp.MustCompile(room.ID)}},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
as.CreateHTTPClient(cfg.AppServiceAPI.DisableTLSValidation)
|
||||||
|
|
||||||
|
// Create a dummy application service
|
||||||
|
cfg.AppServiceAPI.Derived.ApplicationServices = []config.ApplicationService{*as}
|
||||||
|
|
||||||
|
caches := caching.NewRistrettoCache(128*1024*1024, time.Hour, caching.DisableMetrics)
|
||||||
|
// Create required internal APIs
|
||||||
|
rsAPI := roomserver.NewInternalAPI(processCtx, cfg, cm, natsInstance, caches, caching.DisableMetrics)
|
||||||
|
rsAPI.SetFederationAPI(nil, nil)
|
||||||
|
usrAPI := userapi.NewInternalAPI(processCtx, cfg, cm, natsInstance, rsAPI, nil, caching.DisableMetrics, testIsBlacklistedOrBackingOff)
|
||||||
|
// start the consumer
|
||||||
|
appservice.NewInternalAPI(processCtx, cfg, natsInstance, usrAPI, rsAPI)
|
||||||
|
|
||||||
|
// Create the room
|
||||||
|
if err := rsapi.SendEvents(context.Background(), rsAPI, rsapi.KindNew, room.Events(), "test", "test", "test", nil, false); err != nil {
|
||||||
|
t.Fatalf("failed to send events: %v", err)
|
||||||
|
}
|
||||||
|
var seenInvitesForBob int
|
||||||
|
waitLoop:
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-time.After(time.Millisecond * 50): // wait for the AS to process the events
|
||||||
|
break waitLoop
|
||||||
|
case <-evChan:
|
||||||
|
seenInvitesForBob++
|
||||||
|
if seenInvitesForBob != 1 {
|
||||||
|
t.Fatalf("received unexpected invites: %d", seenInvitesForBob)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
close(evChan)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Note: If this test panics, it is because we timed out waiting for the
|
||||||
|
// join event to come through to the appservice and we close the DB/shutdown Dendrite. This makes the
|
||||||
|
// syncAPI unhappy, as it is unable to write to the database.
|
||||||
|
func TestOutputAppserviceEvent(t *testing.T) {
|
||||||
|
alice := test.NewUser(t)
|
||||||
|
bob := test.NewUser(t)
|
||||||
|
|
||||||
|
test.WithAllDatabases(t, func(t *testing.T, dbType test.DBType) {
|
||||||
|
cfg, processCtx, closeDB := testrig.CreateConfig(t, dbType)
|
||||||
|
defer closeDB()
|
||||||
|
cm := sqlutil.NewConnectionManager(processCtx, cfg.Global.DatabaseOptions)
|
||||||
|
natsInstance := &jetstream.NATSInstance{}
|
||||||
|
|
||||||
|
evChan := make(chan struct{})
|
||||||
|
|
||||||
|
caches := caching.NewRistrettoCache(128*1024*1024, time.Hour, caching.DisableMetrics)
|
||||||
|
// Create required internal APIs
|
||||||
|
rsAPI := roomserver.NewInternalAPI(processCtx, cfg, cm, natsInstance, caches, caching.DisableMetrics)
|
||||||
|
rsAPI.SetFederationAPI(nil, nil)
|
||||||
|
|
||||||
|
// Create the router, so we can hit `/joined_members`
|
||||||
|
routers := httputil.NewRouters()
|
||||||
|
|
||||||
|
accessTokens := map[*test.User]userDevice{
|
||||||
|
bob: {},
|
||||||
|
}
|
||||||
|
|
||||||
|
usrAPI := userapi.NewInternalAPI(processCtx, cfg, cm, natsInstance, rsAPI, nil, caching.DisableMetrics, testIsBlacklistedOrBackingOff)
|
||||||
|
clientapi.AddPublicRoutes(processCtx, routers, cfg, natsInstance, nil, rsAPI, nil, nil, nil, usrAPI, nil, nil, caching.DisableMetrics)
|
||||||
|
createAccessTokens(t, accessTokens, usrAPI, processCtx.Context(), routers)
|
||||||
|
|
||||||
|
room := test.NewRoom(t, alice)
|
||||||
|
|
||||||
|
// Invite Bob
|
||||||
|
room.CreateAndInsert(t, alice, spec.MRoomMember, map[string]interface{}{
|
||||||
|
"membership": "invite",
|
||||||
|
}, test.WithStateKey(bob.ID))
|
||||||
|
|
||||||
|
// create a dummy AS url, handling the events
|
||||||
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
var txn consumers.ApplicationServiceTransaction
|
||||||
|
err := json.NewDecoder(r.Body).Decode(&txn)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
for _, ev := range txn.Events {
|
||||||
|
if ev.Type != spec.MRoomMember {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if ev.StateKey != nil && *ev.StateKey == bob.ID {
|
||||||
|
membership := gjson.GetBytes(ev.Content, "membership").Str
|
||||||
|
t.Logf("Processing membership: %s", membership)
|
||||||
|
switch membership {
|
||||||
|
case spec.Invite:
|
||||||
|
// Accept the invite
|
||||||
|
joinEv := room.CreateAndInsert(t, bob, spec.MRoomMember, map[string]interface{}{
|
||||||
|
"membership": "join",
|
||||||
|
}, test.WithStateKey(bob.ID))
|
||||||
|
|
||||||
|
if err := rsapi.SendEvents(context.Background(), rsAPI, rsapi.KindNew, []*types.HeaderedEvent{joinEv}, "test", "test", "test", nil, false); err != nil {
|
||||||
|
t.Fatalf("failed to send events: %v", err)
|
||||||
|
}
|
||||||
|
case spec.Join: // the AS has received the join event, now hit `/joined_members` to validate that
|
||||||
|
rec := httptest.NewRecorder()
|
||||||
|
req := httptest.NewRequest(http.MethodGet, "/_matrix/client/v3/rooms/"+room.ID+"/joined_members", nil)
|
||||||
|
req.Header.Set("Authorization", "Bearer "+accessTokens[bob].accessToken)
|
||||||
|
routers.Client.ServeHTTP(rec, req)
|
||||||
|
if rec.Code != http.StatusOK {
|
||||||
|
t.Fatalf("expected HTTP 200, got %d: %s", rec.Code, rec.Body.String())
|
||||||
|
}
|
||||||
|
|
||||||
|
// Both Alice and Bob should be joined. If not, we have a race condition
|
||||||
|
if !gjson.GetBytes(rec.Body.Bytes(), "joined."+alice.ID).Exists() {
|
||||||
|
t.Errorf("Alice is not joined to the room") // in theory should not happen
|
||||||
|
}
|
||||||
|
if !gjson.GetBytes(rec.Body.Bytes(), "joined."+bob.ID).Exists() {
|
||||||
|
t.Errorf("Bob is not joined to the room")
|
||||||
|
}
|
||||||
|
evChan <- struct{}{}
|
||||||
|
default:
|
||||||
|
t.Fatalf("Unexpected membership: %s", membership)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
defer srv.Close()
|
||||||
|
|
||||||
|
as := &config.ApplicationService{
|
||||||
|
ID: "someID",
|
||||||
|
URL: srv.URL,
|
||||||
|
ASToken: "",
|
||||||
|
HSToken: "",
|
||||||
|
SenderLocalpart: "senderLocalPart",
|
||||||
|
NamespaceMap: map[string][]config.ApplicationServiceNamespace{
|
||||||
|
"users": {{RegexpObject: regexp.MustCompile(bob.ID)}},
|
||||||
|
"aliases": {{RegexpObject: regexp.MustCompile(room.ID)}},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
as.CreateHTTPClient(cfg.AppServiceAPI.DisableTLSValidation)
|
||||||
|
|
||||||
|
// Create a dummy application service
|
||||||
|
cfg.AppServiceAPI.Derived.ApplicationServices = []config.ApplicationService{*as}
|
||||||
|
|
||||||
|
// Prepare AS Streams on the old topic to validate that they get deleted
|
||||||
|
jsCtx, _ := natsInstance.Prepare(processCtx, &cfg.Global.JetStream)
|
||||||
|
|
||||||
|
token := jetstream.Tokenise(as.ID)
|
||||||
|
if err := jetstream.JetStreamConsumer(
|
||||||
|
processCtx.Context(), jsCtx, cfg.Global.JetStream.Prefixed(jetstream.OutputRoomEvent),
|
||||||
|
cfg.Global.JetStream.Durable("Appservice_"+token),
|
||||||
|
50, // maximum number of events to send in a single transaction
|
||||||
|
func(ctx context.Context, msgs []*nats.Msg) bool {
|
||||||
|
return true
|
||||||
|
},
|
||||||
|
); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Start the syncAPI to have `/joined_members` available
|
||||||
|
syncapi.AddPublicRoutes(processCtx, routers, cfg, cm, natsInstance, usrAPI, rsAPI, caches, caching.DisableMetrics)
|
||||||
|
|
||||||
|
// start the consumer
|
||||||
|
appservice.NewInternalAPI(processCtx, cfg, natsInstance, usrAPI, rsAPI)
|
||||||
|
|
||||||
|
// At this point, the old JetStream consumers should be deleted
|
||||||
|
for consumer := range jsCtx.Consumers(cfg.Global.JetStream.Prefixed(jetstream.OutputRoomEvent)) {
|
||||||
|
if consumer.Name == cfg.Global.JetStream.Durable("Appservice_"+token)+"Pull" {
|
||||||
|
t.Fatalf("Consumer still exists")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create the room, this triggers the AS to receive an invite for Bob.
|
||||||
|
if err := rsapi.SendEvents(context.Background(), rsAPI, rsapi.KindNew, room.Events(), "test", "test", "test", nil, false); err != nil {
|
||||||
|
t.Fatalf("failed to send events: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
select {
|
||||||
|
// Pretty generous timeout duration...
|
||||||
|
case <-time.After(time.Millisecond * 1000): // wait for the AS to process the events
|
||||||
|
t.Errorf("Timed out waiting for join event")
|
||||||
|
case <-evChan:
|
||||||
|
}
|
||||||
|
close(evChan)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
type userDevice struct {
|
||||||
|
accessToken string
|
||||||
|
deviceID string
|
||||||
|
password string
|
||||||
|
}
|
||||||
|
|
||||||
|
func createAccessTokens(t *testing.T, accessTokens map[*test.User]userDevice, userAPI uapi.UserInternalAPI, ctx context.Context, routers httputil.Routers) {
|
||||||
|
t.Helper()
|
||||||
|
for u := range accessTokens {
|
||||||
|
localpart, serverName, _ := gomatrixserverlib.SplitID('@', u.ID)
|
||||||
|
userRes := &uapi.PerformAccountCreationResponse{}
|
||||||
|
password := util.RandomString(8)
|
||||||
|
if err := userAPI.PerformAccountCreation(ctx, &uapi.PerformAccountCreationRequest{
|
||||||
|
AccountType: u.AccountType,
|
||||||
|
Localpart: localpart,
|
||||||
|
ServerName: serverName,
|
||||||
|
Password: password,
|
||||||
|
}, userRes); err != nil {
|
||||||
|
t.Errorf("failed to create account: %s", err)
|
||||||
|
}
|
||||||
|
req := test.NewRequest(t, http.MethodPost, "/_matrix/client/v3/login", test.WithJSONBody(t, map[string]interface{}{
|
||||||
|
"type": authtypes.LoginTypePassword,
|
||||||
|
"identifier": map[string]interface{}{
|
||||||
|
"type": "m.id.user",
|
||||||
|
"user": u.ID,
|
||||||
|
},
|
||||||
|
"password": password,
|
||||||
|
}))
|
||||||
|
rec := httptest.NewRecorder()
|
||||||
|
routers.Client.ServeHTTP(rec, req)
|
||||||
|
if rec.Code != http.StatusOK {
|
||||||
|
t.Fatalf("failed to login: %s", rec.Body.String())
|
||||||
|
}
|
||||||
|
accessTokens[u] = userDevice{
|
||||||
|
accessToken: gjson.GetBytes(rec.Body.Bytes(), "access_token").String(),
|
||||||
|
deviceID: gjson.GetBytes(rec.Body.Bytes(), "device_id").String(),
|
||||||
|
password: password,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -26,21 +26,29 @@ import (
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/matrix-org/gomatrixserverlib"
|
"github.com/matrix-org/gomatrixserverlib"
|
||||||
|
"github.com/matrix-org/gomatrixserverlib/spec"
|
||||||
"github.com/nats-io/nats.go"
|
"github.com/nats-io/nats.go"
|
||||||
|
|
||||||
"github.com/matrix-org/dendrite/roomserver/api"
|
"github.com/matrix-org/dendrite/roomserver/api"
|
||||||
|
"github.com/matrix-org/dendrite/roomserver/types"
|
||||||
"github.com/matrix-org/dendrite/setup/config"
|
"github.com/matrix-org/dendrite/setup/config"
|
||||||
"github.com/matrix-org/dendrite/setup/jetstream"
|
"github.com/matrix-org/dendrite/setup/jetstream"
|
||||||
"github.com/matrix-org/dendrite/setup/process"
|
"github.com/matrix-org/dendrite/setup/process"
|
||||||
|
"github.com/matrix-org/dendrite/syncapi/synctypes"
|
||||||
|
|
||||||
log "github.com/sirupsen/logrus"
|
log "github.com/sirupsen/logrus"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// ApplicationServiceTransaction is the transaction that is sent off to an
|
||||||
|
// application service.
|
||||||
|
type ApplicationServiceTransaction struct {
|
||||||
|
Events []synctypes.ClientEvent `json:"events"`
|
||||||
|
}
|
||||||
|
|
||||||
// OutputRoomEventConsumer consumes events that originated in the room server.
|
// OutputRoomEventConsumer consumes events that originated in the room server.
|
||||||
type OutputRoomEventConsumer struct {
|
type OutputRoomEventConsumer struct {
|
||||||
ctx context.Context
|
ctx context.Context
|
||||||
cfg *config.AppServiceAPI
|
cfg *config.AppServiceAPI
|
||||||
client *http.Client
|
|
||||||
jetstream nats.JetStreamContext
|
jetstream nats.JetStreamContext
|
||||||
topic string
|
topic string
|
||||||
rsAPI api.AppserviceRoomserverAPI
|
rsAPI api.AppserviceRoomserverAPI
|
||||||
|
@ -56,22 +64,21 @@ type appserviceState struct {
|
||||||
func NewOutputRoomEventConsumer(
|
func NewOutputRoomEventConsumer(
|
||||||
process *process.ProcessContext,
|
process *process.ProcessContext,
|
||||||
cfg *config.AppServiceAPI,
|
cfg *config.AppServiceAPI,
|
||||||
client *http.Client,
|
|
||||||
js nats.JetStreamContext,
|
js nats.JetStreamContext,
|
||||||
rsAPI api.AppserviceRoomserverAPI,
|
rsAPI api.AppserviceRoomserverAPI,
|
||||||
) *OutputRoomEventConsumer {
|
) *OutputRoomEventConsumer {
|
||||||
return &OutputRoomEventConsumer{
|
return &OutputRoomEventConsumer{
|
||||||
ctx: process.Context(),
|
ctx: process.Context(),
|
||||||
cfg: cfg,
|
cfg: cfg,
|
||||||
client: client,
|
|
||||||
jetstream: js,
|
jetstream: js,
|
||||||
topic: cfg.Matrix.JetStream.Prefixed(jetstream.OutputRoomEvent),
|
topic: cfg.Matrix.JetStream.Prefixed(jetstream.OutputAppserviceEvent),
|
||||||
rsAPI: rsAPI,
|
rsAPI: rsAPI,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Start consuming from room servers
|
// Start consuming from room servers
|
||||||
func (s *OutputRoomEventConsumer) Start() error {
|
func (s *OutputRoomEventConsumer) Start() error {
|
||||||
|
durableNames := make([]string, 0, len(s.cfg.Derived.ApplicationServices))
|
||||||
for _, as := range s.cfg.Derived.ApplicationServices {
|
for _, as := range s.cfg.Derived.ApplicationServices {
|
||||||
appsvc := as
|
appsvc := as
|
||||||
state := &appserviceState{
|
state := &appserviceState{
|
||||||
|
@ -89,6 +96,15 @@ func (s *OutputRoomEventConsumer) Start() error {
|
||||||
); err != nil {
|
); err != nil {
|
||||||
return fmt.Errorf("failed to create %q consumer: %w", token, err)
|
return fmt.Errorf("failed to create %q consumer: %w", token, err)
|
||||||
}
|
}
|
||||||
|
durableNames = append(durableNames, s.cfg.Matrix.JetStream.Durable("Appservice_"+token))
|
||||||
|
}
|
||||||
|
// Cleanup any consumers still existing on the OutputRoomEvent stream
|
||||||
|
// to avoid messages not being deleted
|
||||||
|
for _, consumerName := range durableNames {
|
||||||
|
err := s.jetstream.DeleteConsumer(s.cfg.Matrix.JetStream.Prefixed(jetstream.OutputRoomEvent), consumerName+"Pull")
|
||||||
|
if err != nil && err != nats.ErrConsumerNotFound {
|
||||||
|
return err
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
@ -99,7 +115,7 @@ func (s *OutputRoomEventConsumer) onMessage(
|
||||||
ctx context.Context, state *appserviceState, msgs []*nats.Msg,
|
ctx context.Context, state *appserviceState, msgs []*nats.Msg,
|
||||||
) bool {
|
) bool {
|
||||||
log.WithField("appservice", state.ID).Tracef("Appservice worker received %d message(s) from roomserver", len(msgs))
|
log.WithField("appservice", state.ID).Tracef("Appservice worker received %d message(s) from roomserver", len(msgs))
|
||||||
events := make([]*gomatrixserverlib.HeaderedEvent, 0, len(msgs))
|
events := make([]*types.HeaderedEvent, 0, len(msgs))
|
||||||
for _, msg := range msgs {
|
for _, msg := range msgs {
|
||||||
// Only handle events we care about
|
// Only handle events we care about
|
||||||
receivedType := api.OutputType(msg.Header.Get(jetstream.RoomEventType))
|
receivedType := api.OutputType(msg.Header.Get(jetstream.RoomEventType))
|
||||||
|
@ -122,7 +138,7 @@ func (s *OutputRoomEventConsumer) onMessage(
|
||||||
if len(output.NewRoomEvent.AddsStateEventIDs) > 0 {
|
if len(output.NewRoomEvent.AddsStateEventIDs) > 0 {
|
||||||
newEventID := output.NewRoomEvent.Event.EventID()
|
newEventID := output.NewRoomEvent.Event.EventID()
|
||||||
eventsReq := &api.QueryEventsByIDRequest{
|
eventsReq := &api.QueryEventsByIDRequest{
|
||||||
RoomID: output.NewRoomEvent.Event.RoomID(),
|
RoomID: output.NewRoomEvent.Event.RoomID().String(),
|
||||||
EventIDs: make([]string, 0, len(output.NewRoomEvent.AddsStateEventIDs)),
|
EventIDs: make([]string, 0, len(output.NewRoomEvent.AddsStateEventIDs)),
|
||||||
}
|
}
|
||||||
eventsRes := &api.QueryEventsByIDResponse{}
|
eventsRes := &api.QueryEventsByIDResponse{}
|
||||||
|
@ -140,12 +156,6 @@ func (s *OutputRoomEventConsumer) onMessage(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
case api.OutputTypeNewInviteEvent:
|
|
||||||
if output.NewInviteEvent == nil || !s.appserviceIsInterestedInEvent(ctx, output.NewInviteEvent.Event, state.ApplicationService) {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
events = append(events, output.NewInviteEvent.Event)
|
|
||||||
|
|
||||||
default:
|
default:
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
@ -175,13 +185,15 @@ func (s *OutputRoomEventConsumer) onMessage(
|
||||||
// endpoint. It will block for the backoff period if necessary.
|
// endpoint. It will block for the backoff period if necessary.
|
||||||
func (s *OutputRoomEventConsumer) sendEvents(
|
func (s *OutputRoomEventConsumer) sendEvents(
|
||||||
ctx context.Context, state *appserviceState,
|
ctx context.Context, state *appserviceState,
|
||||||
events []*gomatrixserverlib.HeaderedEvent,
|
events []*types.HeaderedEvent,
|
||||||
txnID string,
|
txnID string,
|
||||||
) error {
|
) error {
|
||||||
// Create the transaction body.
|
// Create the transaction body.
|
||||||
transaction, err := json.Marshal(
|
transaction, err := json.Marshal(
|
||||||
gomatrixserverlib.ApplicationServiceTransaction{
|
ApplicationServiceTransaction{
|
||||||
Events: gomatrixserverlib.HeaderedToClientEvents(events, gomatrixserverlib.FormatAll),
|
Events: synctypes.ToClientEvents(gomatrixserverlib.ToPDUs(events), synctypes.FormatAll, func(roomID spec.RoomID, senderID spec.SenderID) (*spec.UserID, error) {
|
||||||
|
return s.rsAPI.QueryUserIDForSender(ctx, roomID, senderID)
|
||||||
|
}),
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -190,18 +202,26 @@ func (s *OutputRoomEventConsumer) sendEvents(
|
||||||
|
|
||||||
// If txnID is not defined, generate one from the events.
|
// If txnID is not defined, generate one from the events.
|
||||||
if txnID == "" {
|
if txnID == "" {
|
||||||
txnID = fmt.Sprintf("%d_%d", events[0].Event.OriginServerTS(), len(transaction))
|
txnID = fmt.Sprintf("%d_%d", events[0].PDU.OriginServerTS(), len(transaction))
|
||||||
}
|
}
|
||||||
|
|
||||||
// Send the transaction to the appservice.
|
// Send the transaction to the appservice.
|
||||||
// https://matrix.org/docs/spec/application_service/r0.1.2#put-matrix-app-v1-transactions-txnid
|
// https://spec.matrix.org/v1.9/application-service-api/#pushing-events
|
||||||
address := fmt.Sprintf("%s/transactions/%s?access_token=%s", state.URL, txnID, url.QueryEscape(state.HSToken))
|
path := "_matrix/app/v1/transactions"
|
||||||
|
if s.cfg.LegacyPaths {
|
||||||
|
path = "transactions"
|
||||||
|
}
|
||||||
|
address := fmt.Sprintf("%s/%s/%s", state.RequestUrl(), path, txnID)
|
||||||
|
if s.cfg.LegacyAuth {
|
||||||
|
address += "?access_token=" + url.QueryEscape(state.HSToken)
|
||||||
|
}
|
||||||
req, err := http.NewRequestWithContext(ctx, "PUT", address, bytes.NewBuffer(transaction))
|
req, err := http.NewRequestWithContext(ctx, "PUT", address, bytes.NewBuffer(transaction))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
req.Header.Set("Content-Type", "application/json")
|
req.Header.Set("Content-Type", "application/json")
|
||||||
resp, err := s.client.Do(req)
|
req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", state.HSToken))
|
||||||
|
resp, err := state.HTTPClient.Do(req)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return state.backoffAndPause(err)
|
return state.backoffAndPause(err)
|
||||||
}
|
}
|
||||||
|
@ -212,7 +232,7 @@ func (s *OutputRoomEventConsumer) sendEvents(
|
||||||
case http.StatusOK:
|
case http.StatusOK:
|
||||||
state.backoff = 0
|
state.backoff = 0
|
||||||
default:
|
default:
|
||||||
return state.backoffAndPause(fmt.Errorf("received HTTP status code %d from appservice", resp.StatusCode))
|
return state.backoffAndPause(fmt.Errorf("received HTTP status code %d from appservice url %s", resp.StatusCode, address))
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
@ -232,24 +252,30 @@ func (s *appserviceState) backoffAndPause(err error) error {
|
||||||
// event falls within one of a given application service's namespaces.
|
// event falls within one of a given application service's namespaces.
|
||||||
//
|
//
|
||||||
// TODO: This should be cached, see https://github.com/matrix-org/dendrite/issues/1682
|
// TODO: This should be cached, see https://github.com/matrix-org/dendrite/issues/1682
|
||||||
func (s *OutputRoomEventConsumer) appserviceIsInterestedInEvent(ctx context.Context, event *gomatrixserverlib.HeaderedEvent, appservice *config.ApplicationService) bool {
|
func (s *OutputRoomEventConsumer) appserviceIsInterestedInEvent(ctx context.Context, event *types.HeaderedEvent, appservice *config.ApplicationService) bool {
|
||||||
|
user := ""
|
||||||
|
userID, err := s.rsAPI.QueryUserIDForSender(ctx, event.RoomID(), event.SenderID())
|
||||||
|
if err == nil {
|
||||||
|
user = userID.String()
|
||||||
|
}
|
||||||
|
|
||||||
switch {
|
switch {
|
||||||
case appservice.URL == "":
|
case appservice.URL == "":
|
||||||
return false
|
return false
|
||||||
case appservice.IsInterestedInUserID(event.Sender()):
|
case appservice.IsInterestedInUserID(user):
|
||||||
return true
|
return true
|
||||||
case appservice.IsInterestedInRoomID(event.RoomID()):
|
case appservice.IsInterestedInRoomID(event.RoomID().String()):
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
if event.Type() == gomatrixserverlib.MRoomMember && event.StateKey() != nil {
|
if event.Type() == spec.MRoomMember && event.StateKey() != nil {
|
||||||
if appservice.IsInterestedInUserID(*event.StateKey()) {
|
if appservice.IsInterestedInUserID(*event.StateKey()) {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check all known room aliases of the room the event came from
|
// Check all known room aliases of the room the event came from
|
||||||
queryReq := api.GetAliasesForRoomIDRequest{RoomID: event.RoomID()}
|
queryReq := api.GetAliasesForRoomIDRequest{RoomID: event.RoomID().String()}
|
||||||
var queryRes api.GetAliasesForRoomIDResponse
|
var queryRes api.GetAliasesForRoomIDResponse
|
||||||
if err := s.rsAPI.GetAliasesForRoomID(ctx, &queryReq, &queryRes); err == nil {
|
if err := s.rsAPI.GetAliasesForRoomID(ctx, &queryReq, &queryRes); err == nil {
|
||||||
for _, alias := range queryRes.Aliases {
|
for _, alias := range queryRes.Aliases {
|
||||||
|
@ -260,7 +286,7 @@ func (s *OutputRoomEventConsumer) appserviceIsInterestedInEvent(ctx context.Cont
|
||||||
} else {
|
} else {
|
||||||
log.WithFields(log.Fields{
|
log.WithFields(log.Fields{
|
||||||
"appservice": appservice.ID,
|
"appservice": appservice.ID,
|
||||||
"room_id": event.RoomID(),
|
"room_id": event.RoomID().String(),
|
||||||
}).WithError(err).Errorf("Unable to get aliases for room")
|
}).WithError(err).Errorf("Unable to get aliases for room")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -270,13 +296,13 @@ func (s *OutputRoomEventConsumer) appserviceIsInterestedInEvent(ctx context.Cont
|
||||||
|
|
||||||
// appserviceJoinedAtEvent returns a boolean depending on whether a given
|
// appserviceJoinedAtEvent returns a boolean depending on whether a given
|
||||||
// appservice has membership at the time a given event was created.
|
// appservice has membership at the time a given event was created.
|
||||||
func (s *OutputRoomEventConsumer) appserviceJoinedAtEvent(ctx context.Context, event *gomatrixserverlib.HeaderedEvent, appservice *config.ApplicationService) bool {
|
func (s *OutputRoomEventConsumer) appserviceJoinedAtEvent(ctx context.Context, event *types.HeaderedEvent, appservice *config.ApplicationService) bool {
|
||||||
// TODO: This is only checking the current room state, not the state at
|
// TODO: This is only checking the current room state, not the state at
|
||||||
// the event in question. Pretty sure this is what Synapse does too, but
|
// the event in question. Pretty sure this is what Synapse does too, but
|
||||||
// until we have a lighter way of checking the state before the event that
|
// until we have a lighter way of checking the state before the event that
|
||||||
// doesn't involve state res, then this is probably OK.
|
// doesn't involve state res, then this is probably OK.
|
||||||
membershipReq := &api.QueryMembershipsForRoomRequest{
|
membershipReq := &api.QueryMembershipsForRoomRequest{
|
||||||
RoomID: event.RoomID(),
|
RoomID: event.RoomID().String(),
|
||||||
JoinedOnly: true,
|
JoinedOnly: true,
|
||||||
}
|
}
|
||||||
membershipRes := &api.QueryMembershipsForRoomResponse{}
|
membershipRes := &api.QueryMembershipsForRoomResponse{}
|
||||||
|
@ -288,7 +314,7 @@ func (s *OutputRoomEventConsumer) appserviceJoinedAtEvent(ctx context.Context, e
|
||||||
switch {
|
switch {
|
||||||
case ev.StateKey == nil:
|
case ev.StateKey == nil:
|
||||||
continue
|
continue
|
||||||
case ev.Type != gomatrixserverlib.MRoomMember:
|
case ev.Type != spec.MRoomMember:
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
var membership gomatrixserverlib.MemberContent
|
var membership gomatrixserverlib.MemberContent
|
||||||
|
@ -296,7 +322,7 @@ func (s *OutputRoomEventConsumer) appserviceJoinedAtEvent(ctx context.Context, e
|
||||||
switch {
|
switch {
|
||||||
case err != nil:
|
case err != nil:
|
||||||
continue
|
continue
|
||||||
case membership.Membership == gomatrixserverlib.Join:
|
case membership.Membership == spec.Join:
|
||||||
if appservice.IsInterestedInUserID(*ev.StateKey) {
|
if appservice.IsInterestedInUserID(*ev.StateKey) {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
@ -305,7 +331,7 @@ func (s *OutputRoomEventConsumer) appserviceJoinedAtEvent(ctx context.Context, e
|
||||||
} else {
|
} else {
|
||||||
log.WithFields(log.Fields{
|
log.WithFields(log.Fields{
|
||||||
"appservice": appservice.ID,
|
"appservice": appservice.ID,
|
||||||
"room_id": event.RoomID(),
|
"room_id": event.RoomID().String(),
|
||||||
}).WithError(err).Errorf("Unable to get membership for room")
|
}).WithError(err).Errorf("Unable to get membership for room")
|
||||||
}
|
}
|
||||||
return false
|
return false
|
||||||
|
|
|
@ -19,10 +19,10 @@ package query
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
"net/url"
|
||||||
"strings"
|
|
||||||
"sync"
|
"sync"
|
||||||
|
|
||||||
log "github.com/sirupsen/logrus"
|
log "github.com/sirupsen/logrus"
|
||||||
|
@ -32,12 +32,8 @@ import (
|
||||||
"github.com/matrix-org/dendrite/setup/config"
|
"github.com/matrix-org/dendrite/setup/config"
|
||||||
)
|
)
|
||||||
|
|
||||||
const roomAliasExistsPath = "/rooms/"
|
|
||||||
const userIDExistsPath = "/users/"
|
|
||||||
|
|
||||||
// AppServiceQueryAPI is an implementation of api.AppServiceQueryAPI
|
// AppServiceQueryAPI is an implementation of api.AppServiceQueryAPI
|
||||||
type AppServiceQueryAPI struct {
|
type AppServiceQueryAPI struct {
|
||||||
HTTPClient *http.Client
|
|
||||||
Cfg *config.AppServiceAPI
|
Cfg *config.AppServiceAPI
|
||||||
ProtocolCache map[string]api.ASProtocolResponse
|
ProtocolCache map[string]api.ASProtocolResponse
|
||||||
CacheMu sync.Mutex
|
CacheMu sync.Mutex
|
||||||
|
@ -56,14 +52,23 @@ func (a *AppServiceQueryAPI) RoomAliasExists(
|
||||||
// Determine which application service should handle this request
|
// Determine which application service should handle this request
|
||||||
for _, appservice := range a.Cfg.Derived.ApplicationServices {
|
for _, appservice := range a.Cfg.Derived.ApplicationServices {
|
||||||
if appservice.URL != "" && appservice.IsInterestedInRoomAlias(request.Alias) {
|
if appservice.URL != "" && appservice.IsInterestedInRoomAlias(request.Alias) {
|
||||||
|
path := api.ASRoomAliasExistsPath
|
||||||
|
if a.Cfg.LegacyPaths {
|
||||||
|
path = api.ASRoomAliasExistsLegacyPath
|
||||||
|
}
|
||||||
// The full path to the rooms API, includes hs token
|
// The full path to the rooms API, includes hs token
|
||||||
URL, err := url.Parse(appservice.URL + roomAliasExistsPath)
|
URL, err := url.Parse(appservice.RequestUrl() + path)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
URL.Path += request.Alias
|
URL.Path += request.Alias
|
||||||
apiURL := URL.String() + "?access_token=" + appservice.HSToken
|
if a.Cfg.LegacyAuth {
|
||||||
|
q := URL.Query()
|
||||||
|
q.Set("access_token", appservice.HSToken)
|
||||||
|
URL.RawQuery = q.Encode()
|
||||||
|
}
|
||||||
|
apiURL := URL.String()
|
||||||
|
|
||||||
// Send a request to each application service. If one responds that it has
|
// Send a request to each application service. If one responds that it has
|
||||||
// created the room, immediately return.
|
// created the room, immediately return.
|
||||||
|
@ -71,9 +76,10 @@ func (a *AppServiceQueryAPI) RoomAliasExists(
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", appservice.HSToken))
|
||||||
req = req.WithContext(ctx)
|
req = req.WithContext(ctx)
|
||||||
|
|
||||||
resp, err := a.HTTPClient.Do(req)
|
resp, err := appservice.HTTPClient.Do(req)
|
||||||
if resp != nil {
|
if resp != nil {
|
||||||
defer func() {
|
defer func() {
|
||||||
err = resp.Body.Close()
|
err = resp.Body.Close()
|
||||||
|
@ -124,12 +130,21 @@ func (a *AppServiceQueryAPI) UserIDExists(
|
||||||
for _, appservice := range a.Cfg.Derived.ApplicationServices {
|
for _, appservice := range a.Cfg.Derived.ApplicationServices {
|
||||||
if appservice.URL != "" && appservice.IsInterestedInUserID(request.UserID) {
|
if appservice.URL != "" && appservice.IsInterestedInUserID(request.UserID) {
|
||||||
// The full path to the rooms API, includes hs token
|
// The full path to the rooms API, includes hs token
|
||||||
URL, err := url.Parse(appservice.URL + userIDExistsPath)
|
path := api.ASUserExistsPath
|
||||||
|
if a.Cfg.LegacyPaths {
|
||||||
|
path = api.ASUserExistsLegacyPath
|
||||||
|
}
|
||||||
|
URL, err := url.Parse(appservice.RequestUrl() + path)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
URL.Path += request.UserID
|
URL.Path += request.UserID
|
||||||
apiURL := URL.String() + "?access_token=" + appservice.HSToken
|
if a.Cfg.LegacyAuth {
|
||||||
|
q := URL.Query()
|
||||||
|
q.Set("access_token", appservice.HSToken)
|
||||||
|
URL.RawQuery = q.Encode()
|
||||||
|
}
|
||||||
|
apiURL := URL.String()
|
||||||
|
|
||||||
// Send a request to each application service. If one responds that it has
|
// Send a request to each application service. If one responds that it has
|
||||||
// created the user, immediately return.
|
// created the user, immediately return.
|
||||||
|
@ -137,7 +152,8 @@ func (a *AppServiceQueryAPI) UserIDExists(
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
resp, err := a.HTTPClient.Do(req.WithContext(ctx))
|
req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", appservice.HSToken))
|
||||||
|
resp, err := appservice.HTTPClient.Do(req.WithContext(ctx))
|
||||||
if resp != nil {
|
if resp != nil {
|
||||||
defer func() {
|
defer func() {
|
||||||
err = resp.Body.Close()
|
err = resp.Body.Close()
|
||||||
|
@ -177,25 +193,22 @@ type thirdpartyResponses interface {
|
||||||
api.ASProtocolResponse | []api.ASUserResponse | []api.ASLocationResponse
|
api.ASProtocolResponse | []api.ASUserResponse | []api.ASLocationResponse
|
||||||
}
|
}
|
||||||
|
|
||||||
func requestDo[T thirdpartyResponses](client *http.Client, url string, response *T) (err error) {
|
func requestDo[T thirdpartyResponses](as *config.ApplicationService, url string, response *T) error {
|
||||||
origURL := url
|
req, err := http.NewRequest(http.MethodGet, url, nil)
|
||||||
// try v1 and unstable appservice endpoints
|
if err != nil {
|
||||||
for _, version := range []string{"v1", "unstable"} {
|
return err
|
||||||
var resp *http.Response
|
|
||||||
var body []byte
|
|
||||||
asURL := strings.Replace(origURL, "unstable", version, 1)
|
|
||||||
resp, err = client.Get(asURL)
|
|
||||||
if err != nil {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
defer resp.Body.Close() // nolint: errcheck
|
|
||||||
body, err = io.ReadAll(resp.Body)
|
|
||||||
if err != nil {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
return json.Unmarshal(body, &response)
|
|
||||||
}
|
}
|
||||||
return err
|
req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", as.HSToken))
|
||||||
|
resp, err := as.HTTPClient.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer resp.Body.Close() // nolint: errcheck
|
||||||
|
body, err := io.ReadAll(resp.Body)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return json.Unmarshal(body, &response)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *AppServiceQueryAPI) Locations(
|
func (a *AppServiceQueryAPI) Locations(
|
||||||
|
@ -208,17 +221,23 @@ func (a *AppServiceQueryAPI) Locations(
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
path := api.ASLocationPath
|
||||||
|
if a.Cfg.LegacyPaths {
|
||||||
|
path = api.ASLocationLegacyPath
|
||||||
|
}
|
||||||
for _, as := range a.Cfg.Derived.ApplicationServices {
|
for _, as := range a.Cfg.Derived.ApplicationServices {
|
||||||
var asLocations []api.ASLocationResponse
|
var asLocations []api.ASLocationResponse
|
||||||
params.Set("access_token", as.HSToken)
|
if a.Cfg.LegacyAuth {
|
||||||
|
params.Set("access_token", as.HSToken)
|
||||||
|
}
|
||||||
|
|
||||||
url := as.URL + api.ASLocationPath
|
url := as.RequestUrl() + path
|
||||||
if req.Protocol != "" {
|
if req.Protocol != "" {
|
||||||
url += "/" + req.Protocol
|
url += "/" + req.Protocol
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := requestDo[[]api.ASLocationResponse](a.HTTPClient, url+"?"+params.Encode(), &asLocations); err != nil {
|
if err := requestDo[[]api.ASLocationResponse](&as, url+"?"+params.Encode(), &asLocations); err != nil {
|
||||||
log.WithError(err).Error("unable to get 'locations' from application service")
|
log.WithError(err).WithField("application_service", as.ID).Error("unable to get 'locations' from application service")
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -243,17 +262,23 @@ func (a *AppServiceQueryAPI) User(
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
path := api.ASUserPath
|
||||||
|
if a.Cfg.LegacyPaths {
|
||||||
|
path = api.ASUserLegacyPath
|
||||||
|
}
|
||||||
for _, as := range a.Cfg.Derived.ApplicationServices {
|
for _, as := range a.Cfg.Derived.ApplicationServices {
|
||||||
var asUsers []api.ASUserResponse
|
var asUsers []api.ASUserResponse
|
||||||
params.Set("access_token", as.HSToken)
|
if a.Cfg.LegacyAuth {
|
||||||
|
params.Set("access_token", as.HSToken)
|
||||||
|
}
|
||||||
|
|
||||||
url := as.URL + api.ASUserPath
|
url := as.RequestUrl() + path
|
||||||
if req.Protocol != "" {
|
if req.Protocol != "" {
|
||||||
url += "/" + req.Protocol
|
url += "/" + req.Protocol
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := requestDo[[]api.ASUserResponse](a.HTTPClient, url+"?"+params.Encode(), &asUsers); err != nil {
|
if err := requestDo[[]api.ASUserResponse](&as, url+"?"+params.Encode(), &asUsers); err != nil {
|
||||||
log.WithError(err).Error("unable to get 'user' from application service")
|
log.WithError(err).WithField("application_service", as.ID).Error("unable to get 'user' from application service")
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -273,6 +298,10 @@ func (a *AppServiceQueryAPI) Protocols(
|
||||||
req *api.ProtocolRequest,
|
req *api.ProtocolRequest,
|
||||||
resp *api.ProtocolResponse,
|
resp *api.ProtocolResponse,
|
||||||
) error {
|
) error {
|
||||||
|
protocolPath := api.ASProtocolPath
|
||||||
|
if a.Cfg.LegacyPaths {
|
||||||
|
protocolPath = api.ASProtocolLegacyPath
|
||||||
|
}
|
||||||
|
|
||||||
// get a single protocol response
|
// get a single protocol response
|
||||||
if req.Protocol != "" {
|
if req.Protocol != "" {
|
||||||
|
@ -290,8 +319,8 @@ func (a *AppServiceQueryAPI) Protocols(
|
||||||
response := api.ASProtocolResponse{}
|
response := api.ASProtocolResponse{}
|
||||||
for _, as := range a.Cfg.Derived.ApplicationServices {
|
for _, as := range a.Cfg.Derived.ApplicationServices {
|
||||||
var proto api.ASProtocolResponse
|
var proto api.ASProtocolResponse
|
||||||
if err := requestDo[api.ASProtocolResponse](a.HTTPClient, as.URL+api.ASProtocolPath+req.Protocol, &proto); err != nil {
|
if err := requestDo[api.ASProtocolResponse](&as, as.RequestUrl()+protocolPath+req.Protocol, &proto); err != nil {
|
||||||
log.WithError(err).Error("unable to get 'protocol' from application service")
|
log.WithError(err).WithField("application_service", as.ID).Error("unable to get 'protocol' from application service")
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -320,8 +349,8 @@ func (a *AppServiceQueryAPI) Protocols(
|
||||||
for _, as := range a.Cfg.Derived.ApplicationServices {
|
for _, as := range a.Cfg.Derived.ApplicationServices {
|
||||||
for _, p := range as.Protocols {
|
for _, p := range as.Protocols {
|
||||||
var proto api.ASProtocolResponse
|
var proto api.ASProtocolResponse
|
||||||
if err := requestDo[api.ASProtocolResponse](a.HTTPClient, as.URL+api.ASProtocolPath+p, &proto); err != nil {
|
if err := requestDo[api.ASProtocolResponse](&as, as.RequestUrl()+protocolPath+p, &proto); err != nil {
|
||||||
log.WithError(err).Error("unable to get 'protocol' from application service")
|
log.WithError(err).WithField("application_service", as.ID).Error("unable to get 'protocol' from application service")
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
existing, ok := response[p]
|
existing, ok := response[p]
|
||||||
|
|
|
@ -944,4 +944,12 @@ rmv remote user can join room with version 10
|
||||||
rmv User can invite remote user to room with version 10
|
rmv User can invite remote user to room with version 10
|
||||||
rmv Remote user can backfill in a room with version 10
|
rmv Remote user can backfill in a room with version 10
|
||||||
rmv Can reject invites over federation for rooms with version 10
|
rmv Can reject invites over federation for rooms with version 10
|
||||||
rmv Can receive redactions from regular users over federation in room version 10
|
rmv Can receive redactions from regular users over federation in room version 10
|
||||||
|
rmv User can create and send/receive messages in a room with version 11
|
||||||
|
rmv local user can join room with version 11
|
||||||
|
rmv User can invite local user to room with version 11
|
||||||
|
rmv remote user can join room with version 11
|
||||||
|
rmv User can invite remote user to room with version 11
|
||||||
|
rmv Remote user can backfill in a room with version 11
|
||||||
|
rmv Can reject invites over federation for rooms with version 11
|
||||||
|
rmv Can receive redactions from regular users over federation in room version 11
|
51
build.cmd
51
build.cmd
|
@ -1,51 +0,0 @@
|
||||||
@echo off
|
|
||||||
|
|
||||||
:ENTRY_POINT
|
|
||||||
setlocal EnableDelayedExpansion
|
|
||||||
|
|
||||||
REM script base dir
|
|
||||||
set SCRIPTDIR=%~dp0
|
|
||||||
set PROJDIR=%SCRIPTDIR:~0,-1%
|
|
||||||
|
|
||||||
REM Put installed packages into ./bin
|
|
||||||
set GOBIN=%PROJDIR%\bin
|
|
||||||
|
|
||||||
set FLAGS=
|
|
||||||
|
|
||||||
REM Check if sources are under Git control
|
|
||||||
if not exist ".git" goto :CHECK_BIN
|
|
||||||
|
|
||||||
REM set BUILD=`git rev-parse --short HEAD \\ ""`
|
|
||||||
FOR /F "tokens=*" %%X IN ('git rev-parse --short HEAD') DO (
|
|
||||||
set BUILD=%%X
|
|
||||||
)
|
|
||||||
|
|
||||||
REM set BRANCH=`(git symbolic-ref --short HEAD \ tr -d \/ ) \\ ""`
|
|
||||||
FOR /F "tokens=*" %%X IN ('git symbolic-ref --short HEAD') DO (
|
|
||||||
set BRANCHRAW=%%X
|
|
||||||
set BRANCH=!BRANCHRAW:/=!
|
|
||||||
)
|
|
||||||
if "%BRANCH%" == "main" set BRANCH=
|
|
||||||
|
|
||||||
set FLAGS=-X github.com/matrix-org/dendrite/internal.branch=%BRANCH% -X github.com/matrix-org/dendrite/internal.build=%BUILD%
|
|
||||||
|
|
||||||
:CHECK_BIN
|
|
||||||
if exist "bin" goto :ALL_SET
|
|
||||||
mkdir "bin"
|
|
||||||
|
|
||||||
:ALL_SET
|
|
||||||
set CGO_ENABLED=1
|
|
||||||
for /D %%P in (cmd\*) do (
|
|
||||||
go build -trimpath -ldflags "%FLAGS%" -v -o ".\bin" ".\%%P"
|
|
||||||
)
|
|
||||||
|
|
||||||
set CGO_ENABLED=0
|
|
||||||
set GOOS=js
|
|
||||||
set GOARCH=wasm
|
|
||||||
go build -trimpath -ldflags "%FLAGS%" -o bin\main.wasm .\cmd\dendritejs-pinecone
|
|
||||||
|
|
||||||
goto :DONE
|
|
||||||
|
|
||||||
:DONE
|
|
||||||
echo Done
|
|
||||||
endlocal
|
|
24
build.sh
24
build.sh
|
@ -1,24 +0,0 @@
|
||||||
#!/bin/sh -eu
|
|
||||||
|
|
||||||
# Put installed packages into ./bin
|
|
||||||
export GOBIN=$PWD/`dirname $0`/bin
|
|
||||||
|
|
||||||
if [ -d ".git" ]
|
|
||||||
then
|
|
||||||
export BUILD=`git rev-parse --short HEAD || ""`
|
|
||||||
export BRANCH=`(git symbolic-ref --short HEAD | tr -d \/ ) || ""`
|
|
||||||
if [ "$BRANCH" = main ]
|
|
||||||
then
|
|
||||||
export BRANCH=""
|
|
||||||
fi
|
|
||||||
|
|
||||||
export FLAGS="-X github.com/matrix-org/dendrite/internal.branch=$BRANCH -X github.com/matrix-org/dendrite/internal.build=$BUILD"
|
|
||||||
else
|
|
||||||
export FLAGS=""
|
|
||||||
fi
|
|
||||||
|
|
||||||
mkdir -p bin
|
|
||||||
|
|
||||||
CGO_ENABLED=1 go build -trimpath -ldflags "$FLAGS" -v -o "bin/" ./cmd/...
|
|
||||||
|
|
||||||
# CGO_ENABLED=0 GOOS=js GOARCH=wasm go build -trimpath -ldflags "$FLAGS" -o bin/main.wasm ./cmd/dendritejs-pinecone
|
|
|
@ -29,12 +29,16 @@ import (
|
||||||
"github.com/matrix-org/dendrite/cmd/dendrite-demo-pinecone/rooms"
|
"github.com/matrix-org/dendrite/cmd/dendrite-demo-pinecone/rooms"
|
||||||
"github.com/matrix-org/dendrite/cmd/dendrite-demo-yggdrasil/signing"
|
"github.com/matrix-org/dendrite/cmd/dendrite-demo-yggdrasil/signing"
|
||||||
"github.com/matrix-org/dendrite/federationapi"
|
"github.com/matrix-org/dendrite/federationapi"
|
||||||
|
"github.com/matrix-org/dendrite/internal/caching"
|
||||||
"github.com/matrix-org/dendrite/internal/httputil"
|
"github.com/matrix-org/dendrite/internal/httputil"
|
||||||
|
"github.com/matrix-org/dendrite/internal/sqlutil"
|
||||||
"github.com/matrix-org/dendrite/roomserver"
|
"github.com/matrix-org/dendrite/roomserver"
|
||||||
"github.com/matrix-org/dendrite/setup"
|
"github.com/matrix-org/dendrite/setup"
|
||||||
"github.com/matrix-org/dendrite/setup/base"
|
|
||||||
"github.com/matrix-org/dendrite/setup/config"
|
"github.com/matrix-org/dendrite/setup/config"
|
||||||
|
"github.com/matrix-org/dendrite/setup/jetstream"
|
||||||
|
"github.com/matrix-org/dendrite/setup/process"
|
||||||
"github.com/matrix-org/dendrite/userapi"
|
"github.com/matrix-org/dendrite/userapi"
|
||||||
|
"github.com/matrix-org/gomatrixserverlib/spec"
|
||||||
|
|
||||||
"github.com/matrix-org/gomatrixserverlib"
|
"github.com/matrix-org/gomatrixserverlib"
|
||||||
|
|
||||||
|
@ -157,9 +161,8 @@ func startup() {
|
||||||
pManager.AddPeer("wss://pinecone.matrix.org/public")
|
pManager.AddPeer("wss://pinecone.matrix.org/public")
|
||||||
|
|
||||||
cfg := &config.Dendrite{}
|
cfg := &config.Dendrite{}
|
||||||
cfg.Defaults(true)
|
cfg.Defaults(config.DefaultOpts{Generate: true, SingleDatabase: false})
|
||||||
cfg.UserAPI.AccountDatabase.ConnectionString = "file:/idb/dendritejs_account.db"
|
cfg.UserAPI.AccountDatabase.ConnectionString = "file:/idb/dendritejs_account.db"
|
||||||
cfg.AppServiceAPI.Database.ConnectionString = "file:/idb/dendritejs_appservice.db"
|
|
||||||
cfg.FederationAPI.Database.ConnectionString = "file:/idb/dendritejs_fedsender.db"
|
cfg.FederationAPI.Database.ConnectionString = "file:/idb/dendritejs_fedsender.db"
|
||||||
cfg.MediaAPI.Database.ConnectionString = "file:/idb/dendritejs_mediaapi.db"
|
cfg.MediaAPI.Database.ConnectionString = "file:/idb/dendritejs_mediaapi.db"
|
||||||
cfg.RoomServer.Database.ConnectionString = "file:/idb/dendritejs_roomserver.db"
|
cfg.RoomServer.Database.ConnectionString = "file:/idb/dendritejs_roomserver.db"
|
||||||
|
@ -169,35 +172,37 @@ func startup() {
|
||||||
cfg.Global.TrustedIDServers = []string{}
|
cfg.Global.TrustedIDServers = []string{}
|
||||||
cfg.Global.KeyID = gomatrixserverlib.KeyID(signing.KeyID)
|
cfg.Global.KeyID = gomatrixserverlib.KeyID(signing.KeyID)
|
||||||
cfg.Global.PrivateKey = sk
|
cfg.Global.PrivateKey = sk
|
||||||
cfg.Global.ServerName = gomatrixserverlib.ServerName(hex.EncodeToString(pk))
|
cfg.Global.ServerName = spec.ServerName(hex.EncodeToString(pk))
|
||||||
cfg.ClientAPI.RegistrationDisabled = false
|
cfg.ClientAPI.RegistrationDisabled = false
|
||||||
cfg.ClientAPI.OpenRegistrationWithoutVerificationEnabled = true
|
cfg.ClientAPI.OpenRegistrationWithoutVerificationEnabled = true
|
||||||
|
|
||||||
if err := cfg.Derive(); err != nil {
|
if err := cfg.Derive(); err != nil {
|
||||||
logrus.Fatalf("Failed to derive values from config: %s", err)
|
logrus.Fatalf("Failed to derive values from config: %s", err)
|
||||||
}
|
}
|
||||||
base := base.NewBaseDendrite(cfg)
|
natsInstance := jetstream.NATSInstance{}
|
||||||
defer base.Close() // nolint: errcheck
|
processCtx := process.NewProcessContext()
|
||||||
|
cm := sqlutil.NewConnectionManager(processCtx, cfg.Global.DatabaseOptions)
|
||||||
|
routers := httputil.NewRouters()
|
||||||
|
caches := caching.NewRistrettoCache(cfg.Global.Cache.EstimatedMaxSize, cfg.Global.Cache.MaxAge, caching.EnableMetrics)
|
||||||
|
rsAPI := roomserver.NewInternalAPI(processCtx, cfg, cm, &natsInstance, caches, caching.EnableMetrics)
|
||||||
|
|
||||||
rsAPI := roomserver.NewInternalAPI(base)
|
federation := conn.CreateFederationClient(cfg, pSessions)
|
||||||
|
|
||||||
federation := conn.CreateFederationClient(base, pSessions)
|
|
||||||
|
|
||||||
serverKeyAPI := &signing.YggdrasilKeys{}
|
serverKeyAPI := &signing.YggdrasilKeys{}
|
||||||
keyRing := serverKeyAPI.KeyRing()
|
keyRing := serverKeyAPI.KeyRing()
|
||||||
|
|
||||||
userAPI := userapi.NewInternalAPI(base, rsAPI, federation)
|
fedSenderAPI := federationapi.NewInternalAPI(processCtx, cfg, cm, &natsInstance, federation, rsAPI, caches, keyRing, true)
|
||||||
|
userAPI := userapi.NewInternalAPI(processCtx, cfg, cm, &natsInstance, rsAPI, federation, caching.EnableMetrics, fedSenderAPI.IsBlacklistedOrBackingOff)
|
||||||
|
|
||||||
asQuery := appservice.NewInternalAPI(
|
asQuery := appservice.NewInternalAPI(
|
||||||
base, userAPI, rsAPI,
|
processCtx, cfg, &natsInstance, userAPI, rsAPI,
|
||||||
)
|
)
|
||||||
rsAPI.SetAppserviceAPI(asQuery)
|
rsAPI.SetAppserviceAPI(asQuery)
|
||||||
fedSenderAPI := federationapi.NewInternalAPI(base, federation, rsAPI, base.Caches, keyRing, true)
|
|
||||||
rsAPI.SetFederationAPI(fedSenderAPI, keyRing)
|
rsAPI.SetFederationAPI(fedSenderAPI, keyRing)
|
||||||
|
|
||||||
monolith := setup.Monolith{
|
monolith := setup.Monolith{
|
||||||
Config: base.Cfg,
|
Config: cfg,
|
||||||
Client: conn.CreateClient(base, pSessions),
|
Client: conn.CreateClient(pSessions),
|
||||||
FedClient: federation,
|
FedClient: federation,
|
||||||
KeyRing: keyRing,
|
KeyRing: keyRing,
|
||||||
|
|
||||||
|
@ -208,15 +213,15 @@ func startup() {
|
||||||
//ServerKeyAPI: serverKeyAPI,
|
//ServerKeyAPI: serverKeyAPI,
|
||||||
ExtPublicRoomsProvider: rooms.NewPineconeRoomProvider(pRouter, pSessions, fedSenderAPI, federation),
|
ExtPublicRoomsProvider: rooms.NewPineconeRoomProvider(pRouter, pSessions, fedSenderAPI, federation),
|
||||||
}
|
}
|
||||||
monolith.AddAllPublicRoutes(base)
|
monolith.AddAllPublicRoutes(processCtx, cfg, routers, cm, &natsInstance, caches, caching.EnableMetrics)
|
||||||
|
|
||||||
httpRouter := mux.NewRouter().SkipClean(true).UseEncodedPath()
|
httpRouter := mux.NewRouter().SkipClean(true).UseEncodedPath()
|
||||||
httpRouter.PathPrefix(httputil.PublicClientPathPrefix).Handler(base.PublicClientAPIMux)
|
httpRouter.PathPrefix(httputil.PublicClientPathPrefix).Handler(routers.Client)
|
||||||
httpRouter.PathPrefix(httputil.PublicMediaPathPrefix).Handler(base.PublicMediaAPIMux)
|
httpRouter.PathPrefix(httputil.PublicMediaPathPrefix).Handler(routers.Media)
|
||||||
|
|
||||||
p2pRouter := pSessions.Protocol("matrix").HTTP().Mux()
|
p2pRouter := pSessions.Protocol("matrix").HTTP().Mux()
|
||||||
p2pRouter.Handle(httputil.PublicFederationPathPrefix, base.PublicFederationAPIMux)
|
p2pRouter.Handle(httputil.PublicFederationPathPrefix, routers.Federation)
|
||||||
p2pRouter.Handle(httputil.PublicMediaPathPrefix, base.PublicMediaAPIMux)
|
p2pRouter.Handle(httputil.PublicMediaPathPrefix, routers.Media)
|
||||||
|
|
||||||
// Expose the matrix APIs via fetch - for local traffic
|
// Expose the matrix APIs via fetch - for local traffic
|
||||||
go func() {
|
go func() {
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
FROM docker.io/golang:1.19-alpine AS base
|
# Pinned to alpine3.18 until https://github.com/mattn/go-sqlite3/issues/1164 is solved
|
||||||
|
FROM docker.io/golang:1.21-alpine3.18 AS base
|
||||||
|
|
||||||
#
|
#
|
||||||
# Needs to be separate from the main Dockerfile for OpenShift,
|
# Needs to be separate from the main Dockerfile for OpenShift,
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
FROM docker.io/golang:1.19-alpine AS base
|
# Pinned to alpine3.18 until https://github.com/mattn/go-sqlite3/issues/1164 is solved
|
||||||
|
FROM docker.io/golang:1.21-alpine3.18 AS base
|
||||||
|
|
||||||
#
|
#
|
||||||
# Needs to be separate from the main Dockerfile for OpenShift,
|
# Needs to be separate from the main Dockerfile for OpenShift,
|
||||||
|
|
|
@ -6,23 +6,20 @@ They can be found on Docker Hub:
|
||||||
|
|
||||||
- [matrixdotorg/dendrite-monolith](https://hub.docker.com/r/matrixdotorg/dendrite-monolith) for monolith deployments
|
- [matrixdotorg/dendrite-monolith](https://hub.docker.com/r/matrixdotorg/dendrite-monolith) for monolith deployments
|
||||||
|
|
||||||
## Dockerfiles
|
## Dockerfile
|
||||||
|
|
||||||
The `Dockerfile` is a multistage file which can build all four Dendrite
|
The `Dockerfile` is a multistage file which can build Dendrite. From the root of the Dendrite
|
||||||
images depending on the supplied `--target`. From the root of the Dendrite
|
|
||||||
repository, run:
|
repository, run:
|
||||||
|
|
||||||
```
|
```
|
||||||
docker build . --target monolith -t matrixdotorg/dendrite-monolith
|
docker build . -t matrixdotorg/dendrite-monolith
|
||||||
docker build . --target demo-pinecone -t matrixdotorg/dendrite-demo-pinecone
|
|
||||||
docker build . --target demo-yggdrasil -t matrixdotorg/dendrite-demo-yggdrasil
|
|
||||||
```
|
```
|
||||||
|
|
||||||
## Compose files
|
## Compose file
|
||||||
|
|
||||||
There are two sample `docker-compose` files:
|
There is one sample `docker-compose` files:
|
||||||
|
|
||||||
- `docker-compose.monolith.yml` which runs a monolith Dendrite deployment
|
- `docker-compose.yml` which runs a Dendrite deployment with Postgres
|
||||||
|
|
||||||
## Configuration
|
## Configuration
|
||||||
|
|
||||||
|
@ -55,7 +52,7 @@ Create your config based on the [`dendrite-sample.yaml`](https://github.com/matr
|
||||||
Then start the deployment:
|
Then start the deployment:
|
||||||
|
|
||||||
```
|
```
|
||||||
docker-compose -f docker-compose.monolith.yml up
|
docker-compose -f docker-compose.yml up
|
||||||
```
|
```
|
||||||
|
|
||||||
## Building the images
|
## Building the images
|
||||||
|
|
|
@ -1,44 +0,0 @@
|
||||||
version: "3.4"
|
|
||||||
services:
|
|
||||||
postgres:
|
|
||||||
hostname: postgres
|
|
||||||
image: postgres:14
|
|
||||||
restart: always
|
|
||||||
volumes:
|
|
||||||
- ./postgres/create_db.sh:/docker-entrypoint-initdb.d/20-create_db.sh
|
|
||||||
# To persist your PostgreSQL databases outside of the Docker image,
|
|
||||||
# to prevent data loss, modify the following ./path_to path:
|
|
||||||
- ./path_to/postgresql:/var/lib/postgresql/data
|
|
||||||
environment:
|
|
||||||
POSTGRES_PASSWORD: itsasecret
|
|
||||||
POSTGRES_USER: dendrite
|
|
||||||
healthcheck:
|
|
||||||
test: ["CMD-SHELL", "pg_isready -U dendrite"]
|
|
||||||
interval: 5s
|
|
||||||
timeout: 5s
|
|
||||||
retries: 5
|
|
||||||
networks:
|
|
||||||
- internal
|
|
||||||
|
|
||||||
monolith:
|
|
||||||
hostname: monolith
|
|
||||||
image: matrixdotorg/dendrite-monolith:latest
|
|
||||||
command: [
|
|
||||||
"--tls-cert=server.crt",
|
|
||||||
"--tls-key=server.key"
|
|
||||||
]
|
|
||||||
ports:
|
|
||||||
- 8008:8008
|
|
||||||
- 8448:8448
|
|
||||||
volumes:
|
|
||||||
- ./config:/etc/dendrite
|
|
||||||
- ./media:/var/dendrite/media
|
|
||||||
depends_on:
|
|
||||||
- postgres
|
|
||||||
networks:
|
|
||||||
- internal
|
|
||||||
restart: unless-stopped
|
|
||||||
|
|
||||||
networks:
|
|
||||||
internal:
|
|
||||||
attachable: true
|
|
52
build/docker/docker-compose.yml
Normal file
52
build/docker/docker-compose.yml
Normal file
|
@ -0,0 +1,52 @@
|
||||||
|
version: "3.4"
|
||||||
|
|
||||||
|
services:
|
||||||
|
postgres:
|
||||||
|
hostname: postgres
|
||||||
|
image: postgres:15-alpine
|
||||||
|
restart: always
|
||||||
|
volumes:
|
||||||
|
# This will create a docker volume to persist the database files in.
|
||||||
|
# If you prefer those files to be outside of docker, you'll need to change this.
|
||||||
|
- dendrite_postgres_data:/var/lib/postgresql/data
|
||||||
|
environment:
|
||||||
|
POSTGRES_PASSWORD: itsasecret
|
||||||
|
POSTGRES_USER: dendrite
|
||||||
|
POSTGRES_DATABASE: dendrite
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD-SHELL", "pg_isready -U dendrite"]
|
||||||
|
interval: 5s
|
||||||
|
timeout: 5s
|
||||||
|
retries: 5
|
||||||
|
networks:
|
||||||
|
- internal
|
||||||
|
|
||||||
|
monolith:
|
||||||
|
hostname: monolith
|
||||||
|
image: matrixdotorg/dendrite-monolith:latest
|
||||||
|
ports:
|
||||||
|
- 8008:8008
|
||||||
|
- 8448:8448
|
||||||
|
volumes:
|
||||||
|
- ./config:/etc/dendrite
|
||||||
|
# The following volumes use docker volumes, change this
|
||||||
|
# if you prefer to have those files outside of docker.
|
||||||
|
- dendrite_media:/var/dendrite/media
|
||||||
|
- dendrite_jetstream:/var/dendrite/jetstream
|
||||||
|
- dendrite_search_index:/var/dendrite/searchindex
|
||||||
|
depends_on:
|
||||||
|
postgres:
|
||||||
|
condition: service_healthy
|
||||||
|
networks:
|
||||||
|
- internal
|
||||||
|
restart: unless-stopped
|
||||||
|
|
||||||
|
networks:
|
||||||
|
internal:
|
||||||
|
attachable: true
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
dendrite_postgres_data:
|
||||||
|
dendrite_media:
|
||||||
|
dendrite_jetstream:
|
||||||
|
dendrite_search_index:
|
|
@ -1,5 +0,0 @@
|
||||||
#!/bin/sh
|
|
||||||
|
|
||||||
for db in userapi_accounts mediaapi syncapi roomserver keyserver federationapi appservice mscs; do
|
|
||||||
createdb -U dendrite -O dendrite dendrite_$db
|
|
||||||
done
|
|
|
@ -30,8 +30,12 @@ import (
|
||||||
"github.com/matrix-org/dendrite/cmd/dendrite-demo-pinecone/relay"
|
"github.com/matrix-org/dendrite/cmd/dendrite-demo-pinecone/relay"
|
||||||
"github.com/matrix-org/dendrite/cmd/dendrite-demo-yggdrasil/signing"
|
"github.com/matrix-org/dendrite/cmd/dendrite-demo-yggdrasil/signing"
|
||||||
"github.com/matrix-org/dendrite/federationapi/api"
|
"github.com/matrix-org/dendrite/federationapi/api"
|
||||||
|
"github.com/matrix-org/dendrite/internal/httputil"
|
||||||
|
"github.com/matrix-org/dendrite/internal/sqlutil"
|
||||||
|
"github.com/matrix-org/dendrite/setup/process"
|
||||||
userapiAPI "github.com/matrix-org/dendrite/userapi/api"
|
userapiAPI "github.com/matrix-org/dendrite/userapi/api"
|
||||||
"github.com/matrix-org/gomatrixserverlib"
|
"github.com/matrix-org/gomatrixserverlib"
|
||||||
|
"github.com/matrix-org/gomatrixserverlib/spec"
|
||||||
"github.com/matrix-org/pinecone/types"
|
"github.com/matrix-org/pinecone/types"
|
||||||
"github.com/sirupsen/logrus"
|
"github.com/sirupsen/logrus"
|
||||||
|
|
||||||
|
@ -137,9 +141,9 @@ func (m *DendriteMonolith) SetStaticPeer(uri string) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func getServerKeyFromString(nodeID string) (gomatrixserverlib.ServerName, error) {
|
func getServerKeyFromString(nodeID string) (spec.ServerName, error) {
|
||||||
var nodeKey gomatrixserverlib.ServerName
|
var nodeKey spec.ServerName
|
||||||
if userID, err := gomatrixserverlib.NewUserID(nodeID, false); err == nil {
|
if userID, err := spec.NewUserID(nodeID, false); err == nil {
|
||||||
hexKey, decodeErr := hex.DecodeString(string(userID.Domain()))
|
hexKey, decodeErr := hex.DecodeString(string(userID.Domain()))
|
||||||
if decodeErr != nil || len(hexKey) != ed25519.PublicKeySize {
|
if decodeErr != nil || len(hexKey) != ed25519.PublicKeySize {
|
||||||
return "", fmt.Errorf("UserID domain is not a valid ed25519 public key: %v", userID.Domain())
|
return "", fmt.Errorf("UserID domain is not a valid ed25519 public key: %v", userID.Domain())
|
||||||
|
@ -151,7 +155,7 @@ func getServerKeyFromString(nodeID string) (gomatrixserverlib.ServerName, error)
|
||||||
if decodeErr != nil || len(hexKey) != ed25519.PublicKeySize {
|
if decodeErr != nil || len(hexKey) != ed25519.PublicKeySize {
|
||||||
return "", fmt.Errorf("Relay server uri is not a valid ed25519 public key: %v", nodeID)
|
return "", fmt.Errorf("Relay server uri is not a valid ed25519 public key: %v", nodeID)
|
||||||
} else {
|
} else {
|
||||||
nodeKey = gomatrixserverlib.ServerName(nodeID)
|
nodeKey = spec.ServerName(nodeID)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -159,7 +163,7 @@ func getServerKeyFromString(nodeID string) (gomatrixserverlib.ServerName, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *DendriteMonolith) SetRelayServers(nodeID string, uris string) {
|
func (m *DendriteMonolith) SetRelayServers(nodeID string, uris string) {
|
||||||
relays := []gomatrixserverlib.ServerName{}
|
relays := []spec.ServerName{}
|
||||||
for _, uri := range strings.Split(uris, ",") {
|
for _, uri := range strings.Split(uris, ",") {
|
||||||
uri = strings.TrimSpace(uri)
|
uri = strings.TrimSpace(uri)
|
||||||
if len(uri) == 0 {
|
if len(uri) == 0 {
|
||||||
|
@ -185,9 +189,9 @@ func (m *DendriteMonolith) SetRelayServers(nodeID string, uris string) {
|
||||||
m.p2pMonolith.RelayRetriever.SetRelayServers(relays)
|
m.p2pMonolith.RelayRetriever.SetRelayServers(relays)
|
||||||
} else {
|
} else {
|
||||||
relay.UpdateNodeRelayServers(
|
relay.UpdateNodeRelayServers(
|
||||||
gomatrixserverlib.ServerName(nodeKey),
|
spec.ServerName(nodeKey),
|
||||||
relays,
|
relays,
|
||||||
m.p2pMonolith.BaseDendrite.Context(),
|
m.p2pMonolith.ProcessCtx.Context(),
|
||||||
m.p2pMonolith.GetFederationAPI(),
|
m.p2pMonolith.GetFederationAPI(),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -212,9 +216,9 @@ func (m *DendriteMonolith) GetRelayServers(nodeID string) string {
|
||||||
relaysString += string(relay)
|
relaysString += string(relay)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
request := api.P2PQueryRelayServersRequest{Server: gomatrixserverlib.ServerName(nodeKey)}
|
request := api.P2PQueryRelayServersRequest{Server: spec.ServerName(nodeKey)}
|
||||||
response := api.P2PQueryRelayServersResponse{}
|
response := api.P2PQueryRelayServersResponse{}
|
||||||
err := m.p2pMonolith.GetFederationAPI().P2PQueryRelayServers(m.p2pMonolith.BaseDendrite.Context(), &request, &response)
|
err := m.p2pMonolith.GetFederationAPI().P2PQueryRelayServers(m.p2pMonolith.ProcessCtx.Context(), &request, &response)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logrus.Warnf("Failed obtaining list of this node's relay servers: %s", err.Error())
|
logrus.Warnf("Failed obtaining list of this node's relay servers: %s", err.Error())
|
||||||
return ""
|
return ""
|
||||||
|
@ -288,7 +292,7 @@ func (m *DendriteMonolith) RegisterUser(localpart, password string) (string, err
|
||||||
pubkey := m.p2pMonolith.Router.PublicKey()
|
pubkey := m.p2pMonolith.Router.PublicKey()
|
||||||
userID := userutil.MakeUserID(
|
userID := userutil.MakeUserID(
|
||||||
localpart,
|
localpart,
|
||||||
gomatrixserverlib.ServerName(hex.EncodeToString(pubkey[:])),
|
spec.ServerName(hex.EncodeToString(pubkey[:])),
|
||||||
)
|
)
|
||||||
userReq := &userapiAPI.PerformAccountCreationRequest{
|
userReq := &userapiAPI.PerformAccountCreationRequest{
|
||||||
AccountType: userapiAPI.AccountTypeUser,
|
AccountType: userapiAPI.AccountTypeUser,
|
||||||
|
@ -339,17 +343,21 @@ func (m *DendriteMonolith) Start() {
|
||||||
|
|
||||||
prefix := hex.EncodeToString(pk)
|
prefix := hex.EncodeToString(pk)
|
||||||
cfg := monolith.GenerateDefaultConfig(sk, m.StorageDirectory, m.CacheDirectory, prefix)
|
cfg := monolith.GenerateDefaultConfig(sk, m.StorageDirectory, m.CacheDirectory, prefix)
|
||||||
cfg.Global.ServerName = gomatrixserverlib.ServerName(hex.EncodeToString(pk))
|
cfg.Global.ServerName = spec.ServerName(hex.EncodeToString(pk))
|
||||||
cfg.Global.KeyID = gomatrixserverlib.KeyID(signing.KeyID)
|
cfg.Global.KeyID = gomatrixserverlib.KeyID(signing.KeyID)
|
||||||
cfg.Global.JetStream.InMemory = false
|
cfg.Global.JetStream.InMemory = false
|
||||||
// NOTE : disabled for now since there is a 64 bit alignment panic on 32 bit systems
|
// NOTE : disabled for now since there is a 64 bit alignment panic on 32 bit systems
|
||||||
// This isn't actually fixed: https://github.com/blevesearch/zapx/pull/147
|
// This isn't actually fixed: https://github.com/blevesearch/zapx/pull/147
|
||||||
cfg.SyncAPI.Fulltext.Enabled = false
|
cfg.SyncAPI.Fulltext.Enabled = false
|
||||||
|
|
||||||
|
processCtx := process.NewProcessContext()
|
||||||
|
cm := sqlutil.NewConnectionManager(processCtx, cfg.Global.DatabaseOptions)
|
||||||
|
routers := httputil.NewRouters()
|
||||||
|
|
||||||
enableRelaying := false
|
enableRelaying := false
|
||||||
enableMetrics := false
|
enableMetrics := false
|
||||||
enableWebsockets := false
|
enableWebsockets := false
|
||||||
m.p2pMonolith.SetupDendrite(cfg, 65432, enableRelaying, enableMetrics, enableWebsockets)
|
m.p2pMonolith.SetupDendrite(processCtx, cfg, cm, routers, 65432, enableRelaying, enableMetrics, enableWebsockets)
|
||||||
m.p2pMonolith.StartMonolith()
|
m.p2pMonolith.StartMonolith()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -18,11 +18,14 @@ import (
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/matrix-org/gomatrixserverlib"
|
"github.com/matrix-org/gomatrixserverlib/spec"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestMonolithStarts(t *testing.T) {
|
func TestMonolithStarts(t *testing.T) {
|
||||||
monolith := DendriteMonolith{}
|
monolith := DendriteMonolith{
|
||||||
|
StorageDirectory: t.TempDir(),
|
||||||
|
CacheDirectory: t.TempDir(),
|
||||||
|
}
|
||||||
monolith.Start()
|
monolith.Start()
|
||||||
monolith.PublicKey()
|
monolith.PublicKey()
|
||||||
monolith.Stop()
|
monolith.Stop()
|
||||||
|
@ -60,7 +63,10 @@ func TestMonolithSetRelayServers(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, tc := range testCases {
|
for _, tc := range testCases {
|
||||||
monolith := DendriteMonolith{}
|
monolith := DendriteMonolith{
|
||||||
|
StorageDirectory: t.TempDir(),
|
||||||
|
CacheDirectory: t.TempDir(),
|
||||||
|
}
|
||||||
monolith.Start()
|
monolith.Start()
|
||||||
|
|
||||||
inputRelays := tc.relays
|
inputRelays := tc.relays
|
||||||
|
@ -110,7 +116,7 @@ func TestParseServerKey(t *testing.T) {
|
||||||
name string
|
name string
|
||||||
serverKey string
|
serverKey string
|
||||||
expectedErr bool
|
expectedErr bool
|
||||||
expectedKey gomatrixserverlib.ServerName
|
expectedKey spec.ServerName
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
name: "valid userid as key",
|
name: "valid userid as key",
|
||||||
|
|
|
@ -12,6 +12,7 @@ import (
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/getsentry/sentry-go"
|
||||||
"github.com/gorilla/mux"
|
"github.com/gorilla/mux"
|
||||||
"github.com/matrix-org/dendrite/appservice"
|
"github.com/matrix-org/dendrite/appservice"
|
||||||
"github.com/matrix-org/dendrite/cmd/dendrite-demo-yggdrasil/signing"
|
"github.com/matrix-org/dendrite/cmd/dendrite-demo-yggdrasil/signing"
|
||||||
|
@ -19,15 +20,20 @@ import (
|
||||||
"github.com/matrix-org/dendrite/cmd/dendrite-demo-yggdrasil/yggrooms"
|
"github.com/matrix-org/dendrite/cmd/dendrite-demo-yggdrasil/yggrooms"
|
||||||
"github.com/matrix-org/dendrite/federationapi"
|
"github.com/matrix-org/dendrite/federationapi"
|
||||||
"github.com/matrix-org/dendrite/federationapi/api"
|
"github.com/matrix-org/dendrite/federationapi/api"
|
||||||
|
"github.com/matrix-org/dendrite/internal"
|
||||||
|
"github.com/matrix-org/dendrite/internal/caching"
|
||||||
"github.com/matrix-org/dendrite/internal/httputil"
|
"github.com/matrix-org/dendrite/internal/httputil"
|
||||||
|
"github.com/matrix-org/dendrite/internal/sqlutil"
|
||||||
"github.com/matrix-org/dendrite/roomserver"
|
"github.com/matrix-org/dendrite/roomserver"
|
||||||
"github.com/matrix-org/dendrite/setup"
|
"github.com/matrix-org/dendrite/setup"
|
||||||
"github.com/matrix-org/dendrite/setup/base"
|
basepkg "github.com/matrix-org/dendrite/setup/base"
|
||||||
"github.com/matrix-org/dendrite/setup/config"
|
"github.com/matrix-org/dendrite/setup/config"
|
||||||
|
"github.com/matrix-org/dendrite/setup/jetstream"
|
||||||
"github.com/matrix-org/dendrite/setup/process"
|
"github.com/matrix-org/dendrite/setup/process"
|
||||||
"github.com/matrix-org/dendrite/test"
|
"github.com/matrix-org/dendrite/test"
|
||||||
"github.com/matrix-org/dendrite/userapi"
|
"github.com/matrix-org/dendrite/userapi"
|
||||||
"github.com/matrix-org/gomatrixserverlib"
|
"github.com/matrix-org/gomatrixserverlib"
|
||||||
|
"github.com/matrix-org/gomatrixserverlib/spec"
|
||||||
"github.com/sirupsen/logrus"
|
"github.com/sirupsen/logrus"
|
||||||
|
|
||||||
_ "golang.org/x/mobile/bind"
|
_ "golang.org/x/mobile/bind"
|
||||||
|
@ -129,7 +135,7 @@ func (m *DendriteMonolith) Start() {
|
||||||
Generate: true,
|
Generate: true,
|
||||||
SingleDatabase: true,
|
SingleDatabase: true,
|
||||||
})
|
})
|
||||||
cfg.Global.ServerName = gomatrixserverlib.ServerName(hex.EncodeToString(pk))
|
cfg.Global.ServerName = spec.ServerName(hex.EncodeToString(pk))
|
||||||
cfg.Global.PrivateKey = sk
|
cfg.Global.PrivateKey = sk
|
||||||
cfg.Global.KeyID = gomatrixserverlib.KeyID(signing.KeyID)
|
cfg.Global.KeyID = gomatrixserverlib.KeyID(signing.KeyID)
|
||||||
cfg.Global.JetStream.StoragePath = config.Path(fmt.Sprintf("%s/", m.StorageDirectory))
|
cfg.Global.JetStream.StoragePath = config.Path(fmt.Sprintf("%s/", m.StorageDirectory))
|
||||||
|
@ -148,25 +154,71 @@ func (m *DendriteMonolith) Start() {
|
||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
base := base.NewBaseDendrite(cfg)
|
configErrors := &config.ConfigErrors{}
|
||||||
base.ConfigureAdminEndpoints()
|
cfg.Verify(configErrors)
|
||||||
m.processContext = base.ProcessContext
|
if len(*configErrors) > 0 {
|
||||||
defer base.Close() // nolint: errcheck
|
for _, err := range *configErrors {
|
||||||
|
logrus.Errorf("Configuration error: %s", err)
|
||||||
|
}
|
||||||
|
logrus.Fatalf("Failed to start due to configuration errors")
|
||||||
|
}
|
||||||
|
|
||||||
federation := ygg.CreateFederationClient(base)
|
internal.SetupStdLogging()
|
||||||
|
internal.SetupHookLogging(cfg.Logging)
|
||||||
|
internal.SetupPprof()
|
||||||
|
|
||||||
|
logrus.Infof("Dendrite version %s", internal.VersionString())
|
||||||
|
|
||||||
|
if !cfg.ClientAPI.RegistrationDisabled && cfg.ClientAPI.OpenRegistrationWithoutVerificationEnabled {
|
||||||
|
logrus.Warn("Open registration is enabled")
|
||||||
|
}
|
||||||
|
|
||||||
|
closer, err := cfg.SetupTracing()
|
||||||
|
if err != nil {
|
||||||
|
logrus.WithError(err).Panicf("failed to start opentracing")
|
||||||
|
}
|
||||||
|
defer closer.Close()
|
||||||
|
|
||||||
|
if cfg.Global.Sentry.Enabled {
|
||||||
|
logrus.Info("Setting up Sentry for debugging...")
|
||||||
|
err = sentry.Init(sentry.ClientOptions{
|
||||||
|
Dsn: cfg.Global.Sentry.DSN,
|
||||||
|
Environment: cfg.Global.Sentry.Environment,
|
||||||
|
Debug: true,
|
||||||
|
ServerName: string(cfg.Global.ServerName),
|
||||||
|
Release: "dendrite@" + internal.VersionString(),
|
||||||
|
AttachStacktrace: true,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
logrus.WithError(err).Panic("failed to start Sentry")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
processCtx := process.NewProcessContext()
|
||||||
|
cm := sqlutil.NewConnectionManager(processCtx, cfg.Global.DatabaseOptions)
|
||||||
|
routers := httputil.NewRouters()
|
||||||
|
basepkg.ConfigureAdminEndpoints(processCtx, routers)
|
||||||
|
m.processContext = processCtx
|
||||||
|
defer func() {
|
||||||
|
processCtx.ShutdownDendrite()
|
||||||
|
processCtx.WaitForShutdown()
|
||||||
|
}() // nolint: errcheck
|
||||||
|
|
||||||
|
federation := ygg.CreateFederationClient(cfg)
|
||||||
|
|
||||||
serverKeyAPI := &signing.YggdrasilKeys{}
|
serverKeyAPI := &signing.YggdrasilKeys{}
|
||||||
keyRing := serverKeyAPI.KeyRing()
|
keyRing := serverKeyAPI.KeyRing()
|
||||||
|
|
||||||
rsAPI := roomserver.NewInternalAPI(base)
|
caches := caching.NewRistrettoCache(cfg.Global.Cache.EstimatedMaxSize, cfg.Global.Cache.MaxAge, caching.EnableMetrics)
|
||||||
|
natsInstance := jetstream.NATSInstance{}
|
||||||
|
rsAPI := roomserver.NewInternalAPI(processCtx, cfg, cm, &natsInstance, caches, caching.EnableMetrics)
|
||||||
|
|
||||||
fsAPI := federationapi.NewInternalAPI(
|
fsAPI := federationapi.NewInternalAPI(
|
||||||
base, federation, rsAPI, base.Caches, keyRing, true,
|
processCtx, cfg, cm, &natsInstance, federation, rsAPI, caches, keyRing, true,
|
||||||
)
|
)
|
||||||
|
|
||||||
userAPI := userapi.NewInternalAPI(base, rsAPI, federation)
|
userAPI := userapi.NewInternalAPI(processCtx, cfg, cm, &natsInstance, rsAPI, federation, caching.EnableMetrics, fsAPI.IsBlacklistedOrBackingOff)
|
||||||
|
|
||||||
asAPI := appservice.NewInternalAPI(base, userAPI, rsAPI)
|
asAPI := appservice.NewInternalAPI(processCtx, cfg, &natsInstance, userAPI, rsAPI)
|
||||||
rsAPI.SetAppserviceAPI(asAPI)
|
rsAPI.SetAppserviceAPI(asAPI)
|
||||||
|
|
||||||
// The underlying roomserver implementation needs to be able to call the fedsender.
|
// The underlying roomserver implementation needs to be able to call the fedsender.
|
||||||
|
@ -174,8 +226,8 @@ func (m *DendriteMonolith) Start() {
|
||||||
rsAPI.SetFederationAPI(fsAPI, keyRing)
|
rsAPI.SetFederationAPI(fsAPI, keyRing)
|
||||||
|
|
||||||
monolith := setup.Monolith{
|
monolith := setup.Monolith{
|
||||||
Config: base.Cfg,
|
Config: cfg,
|
||||||
Client: ygg.CreateClient(base),
|
Client: ygg.CreateClient(),
|
||||||
FedClient: federation,
|
FedClient: federation,
|
||||||
KeyRing: keyRing,
|
KeyRing: keyRing,
|
||||||
|
|
||||||
|
@ -187,17 +239,17 @@ func (m *DendriteMonolith) Start() {
|
||||||
ygg, fsAPI, federation,
|
ygg, fsAPI, federation,
|
||||||
),
|
),
|
||||||
}
|
}
|
||||||
monolith.AddAllPublicRoutes(base)
|
monolith.AddAllPublicRoutes(processCtx, cfg, routers, cm, &natsInstance, caches, caching.EnableMetrics)
|
||||||
|
|
||||||
httpRouter := mux.NewRouter()
|
httpRouter := mux.NewRouter()
|
||||||
httpRouter.PathPrefix(httputil.PublicClientPathPrefix).Handler(base.PublicClientAPIMux)
|
httpRouter.PathPrefix(httputil.PublicClientPathPrefix).Handler(routers.Client)
|
||||||
httpRouter.PathPrefix(httputil.PublicMediaPathPrefix).Handler(base.PublicMediaAPIMux)
|
httpRouter.PathPrefix(httputil.PublicMediaPathPrefix).Handler(routers.Media)
|
||||||
httpRouter.PathPrefix(httputil.DendriteAdminPathPrefix).Handler(base.DendriteAdminMux)
|
httpRouter.PathPrefix(httputil.DendriteAdminPathPrefix).Handler(routers.DendriteAdmin)
|
||||||
httpRouter.PathPrefix(httputil.SynapseAdminPathPrefix).Handler(base.SynapseAdminMux)
|
httpRouter.PathPrefix(httputil.SynapseAdminPathPrefix).Handler(routers.SynapseAdmin)
|
||||||
|
|
||||||
yggRouter := mux.NewRouter()
|
yggRouter := mux.NewRouter()
|
||||||
yggRouter.PathPrefix(httputil.PublicFederationPathPrefix).Handler(base.PublicFederationAPIMux)
|
yggRouter.PathPrefix(httputil.PublicFederationPathPrefix).Handler(routers.Federation)
|
||||||
yggRouter.PathPrefix(httputil.PublicMediaPathPrefix).Handler(base.PublicMediaAPIMux)
|
yggRouter.PathPrefix(httputil.PublicMediaPathPrefix).Handler(routers.Media)
|
||||||
|
|
||||||
// Build both ends of a HTTP multiplex.
|
// Build both ends of a HTTP multiplex.
|
||||||
m.httpServer = &http.Server{
|
m.httpServer = &http.Server{
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
#syntax=docker/dockerfile:1.2
|
#syntax=docker/dockerfile:1.2
|
||||||
|
|
||||||
FROM golang:1.18-stretch as build
|
FROM golang:1.20-bullseye as build
|
||||||
RUN apt-get update && apt-get install -y sqlite3
|
RUN apt-get update && apt-get install -y sqlite3
|
||||||
WORKDIR /build
|
WORKDIR /build
|
||||||
|
|
||||||
|
@ -17,7 +17,7 @@ RUN --mount=target=. \
|
||||||
CGO_ENABLED=${CGO} go build -o /dendrite ./cmd/generate-config && \
|
CGO_ENABLED=${CGO} go build -o /dendrite ./cmd/generate-config && \
|
||||||
CGO_ENABLED=${CGO} go build -o /dendrite ./cmd/generate-keys && \
|
CGO_ENABLED=${CGO} go build -o /dendrite ./cmd/generate-keys && \
|
||||||
CGO_ENABLED=${CGO} go build -o /dendrite/dendrite ./cmd/dendrite && \
|
CGO_ENABLED=${CGO} go build -o /dendrite/dendrite ./cmd/dendrite && \
|
||||||
CGO_ENABLED=${CGO} go test -c -cover -covermode=atomic -o /dendrite/dendrite-cover -coverpkg "github.com/matrix-org/..." ./cmd/dendrite && \
|
CGO_ENABLED=${CGO} go build -cover -covermode=atomic -o /dendrite/dendrite-cover -coverpkg "github.com/matrix-org/..." ./cmd/dendrite && \
|
||||||
cp build/scripts/complement-cmd.sh /complement-cmd.sh
|
cp build/scripts/complement-cmd.sh /complement-cmd.sh
|
||||||
|
|
||||||
WORKDIR /dendrite
|
WORKDIR /dendrite
|
||||||
|
|
|
@ -1,19 +1,19 @@
|
||||||
#syntax=docker/dockerfile:1.2
|
#syntax=docker/dockerfile:1.2
|
||||||
|
|
||||||
FROM golang:1.18-stretch as build
|
FROM golang:1.20-bullseye as build
|
||||||
RUN apt-get update && apt-get install -y postgresql
|
RUN apt-get update && apt-get install -y postgresql
|
||||||
WORKDIR /build
|
WORKDIR /build
|
||||||
|
|
||||||
# No password when connecting over localhost
|
# No password when connecting over localhost
|
||||||
RUN sed -i "s%127.0.0.1/32 md5%127.0.0.1/32 trust%g" /etc/postgresql/9.6/main/pg_hba.conf && \
|
RUN sed -i "s%127.0.0.1/32 md5%127.0.0.1/32 trust%g" /etc/postgresql/13/main/pg_hba.conf && \
|
||||||
# Bump up max conns for moar concurrency
|
# Bump up max conns for moar concurrency
|
||||||
sed -i 's/max_connections = 100/max_connections = 2000/g' /etc/postgresql/9.6/main/postgresql.conf
|
sed -i 's/max_connections = 100/max_connections = 2000/g' /etc/postgresql/13/main/postgresql.conf
|
||||||
|
|
||||||
# This entry script starts postgres, waits for it to be up then starts dendrite
|
# This entry script starts postgres, waits for it to be up then starts dendrite
|
||||||
RUN echo '\
|
RUN echo '\
|
||||||
#!/bin/bash -eu \n\
|
#!/bin/bash -eu \n\
|
||||||
pg_lsclusters \n\
|
pg_lsclusters \n\
|
||||||
pg_ctlcluster 9.6 main start \n\
|
pg_ctlcluster 13 main start \n\
|
||||||
\n\
|
\n\
|
||||||
until pg_isready \n\
|
until pg_isready \n\
|
||||||
do \n\
|
do \n\
|
||||||
|
@ -35,7 +35,7 @@ RUN --mount=target=. \
|
||||||
CGO_ENABLED=${CGO} go build -o /dendrite ./cmd/generate-config && \
|
CGO_ENABLED=${CGO} go build -o /dendrite ./cmd/generate-config && \
|
||||||
CGO_ENABLED=${CGO} go build -o /dendrite ./cmd/generate-keys && \
|
CGO_ENABLED=${CGO} go build -o /dendrite ./cmd/generate-keys && \
|
||||||
CGO_ENABLED=${CGO} go build -o /dendrite/dendrite ./cmd/dendrite && \
|
CGO_ENABLED=${CGO} go build -o /dendrite/dendrite ./cmd/dendrite && \
|
||||||
CGO_ENABLED=${CGO} go test -c -cover -covermode=atomic -o /dendrite/dendrite-cover -coverpkg "github.com/matrix-org/..." ./cmd/dendrite && \
|
CGO_ENABLED=${CGO} go build -cover -covermode=atomic -o /dendrite/dendrite-cover -coverpkg "github.com/matrix-org/..." ./cmd/dendrite && \
|
||||||
cp build/scripts/complement-cmd.sh /complement-cmd.sh
|
cp build/scripts/complement-cmd.sh /complement-cmd.sh
|
||||||
|
|
||||||
WORKDIR /dendrite
|
WORKDIR /dendrite
|
||||||
|
|
|
@ -2,14 +2,15 @@
|
||||||
|
|
||||||
# This script is intended to be used inside a docker container for Complement
|
# This script is intended to be used inside a docker container for Complement
|
||||||
|
|
||||||
|
export GOCOVERDIR=/tmp/covdatafiles
|
||||||
|
mkdir -p "${GOCOVERDIR}"
|
||||||
if [[ "${COVER}" -eq 1 ]]; then
|
if [[ "${COVER}" -eq 1 ]]; then
|
||||||
echo "Running with coverage"
|
echo "Running with coverage"
|
||||||
exec /dendrite/dendrite-cover \
|
exec /dendrite/dendrite-cover \
|
||||||
--really-enable-open-registration \
|
--really-enable-open-registration \
|
||||||
--tls-cert server.crt \
|
--tls-cert server.crt \
|
||||||
--tls-key server.key \
|
--tls-key server.key \
|
||||||
--config dendrite.yaml \
|
--config dendrite.yaml
|
||||||
--test.coverprofile=complementcover.log
|
|
||||||
else
|
else
|
||||||
echo "Not running with coverage"
|
echo "Not running with coverage"
|
||||||
exec /dendrite/dendrite \
|
exec /dendrite/dendrite \
|
||||||
|
|
|
@ -15,5 +15,5 @@ tar -xzf master.tar.gz
|
||||||
|
|
||||||
# Run the tests!
|
# Run the tests!
|
||||||
cd complement-master
|
cd complement-master
|
||||||
COMPLEMENT_BASE_IMAGE=complement-dendrite:latest go test -v -count=1 ./tests
|
COMPLEMENT_BASE_IMAGE=complement-dendrite:latest go test -v -count=1 ./tests ./tests/csapi
|
||||||
|
|
||||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -14,10 +14,18 @@
|
||||||
|
|
||||||
package api
|
package api
|
||||||
|
|
||||||
import "github.com/matrix-org/gomatrixserverlib"
|
import "github.com/matrix-org/gomatrixserverlib/fclient"
|
||||||
|
|
||||||
// ExtraPublicRoomsProvider provides a way to inject extra published rooms into /publicRooms requests.
|
// ExtraPublicRoomsProvider provides a way to inject extra published rooms into /publicRooms requests.
|
||||||
type ExtraPublicRoomsProvider interface {
|
type ExtraPublicRoomsProvider interface {
|
||||||
// Rooms returns the extra rooms. This is called on-demand by clients, so cache appropriately.
|
// Rooms returns the extra rooms. This is called on-demand by clients, so cache appropriately.
|
||||||
Rooms() []gomatrixserverlib.PublicRoom
|
Rooms() []fclient.PublicRoom
|
||||||
|
}
|
||||||
|
|
||||||
|
type RegistrationToken struct {
|
||||||
|
Token *string `json:"token"`
|
||||||
|
UsesAllowed *int32 `json:"uses_allowed"`
|
||||||
|
Pending *int32 `json:"pending"`
|
||||||
|
Completed *int32 `json:"completed"`
|
||||||
|
ExpiryTime *int64 `json:"expiry_time"`
|
||||||
}
|
}
|
||||||
|
|
|
@ -23,8 +23,8 @@ import (
|
||||||
"net/http"
|
"net/http"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/matrix-org/dendrite/clientapi/jsonerror"
|
|
||||||
"github.com/matrix-org/dendrite/userapi/api"
|
"github.com/matrix-org/dendrite/userapi/api"
|
||||||
|
"github.com/matrix-org/gomatrixserverlib/spec"
|
||||||
"github.com/matrix-org/util"
|
"github.com/matrix-org/util"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -58,7 +58,7 @@ func VerifyUserFromRequest(
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, &util.JSONResponse{
|
return nil, &util.JSONResponse{
|
||||||
Code: http.StatusUnauthorized,
|
Code: http.StatusUnauthorized,
|
||||||
JSON: jsonerror.MissingToken(err.Error()),
|
JSON: spec.MissingToken(err.Error()),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
var res api.QueryAccessTokenResponse
|
var res api.QueryAccessTokenResponse
|
||||||
|
@ -68,21 +68,23 @@ func VerifyUserFromRequest(
|
||||||
}, &res)
|
}, &res)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
util.GetLogger(req.Context()).WithError(err).Error("userAPI.QueryAccessToken failed")
|
util.GetLogger(req.Context()).WithError(err).Error("userAPI.QueryAccessToken failed")
|
||||||
jsonErr := jsonerror.InternalServerError()
|
return nil, &util.JSONResponse{
|
||||||
return nil, &jsonErr
|
Code: http.StatusInternalServerError,
|
||||||
|
JSON: spec.InternalServerError{},
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if res.Err != "" {
|
if res.Err != "" {
|
||||||
if strings.HasPrefix(strings.ToLower(res.Err), "forbidden:") { // TODO: use actual error and no string comparison
|
if strings.HasPrefix(strings.ToLower(res.Err), "forbidden:") { // TODO: use actual error and no string comparison
|
||||||
return nil, &util.JSONResponse{
|
return nil, &util.JSONResponse{
|
||||||
Code: http.StatusForbidden,
|
Code: http.StatusForbidden,
|
||||||
JSON: jsonerror.Forbidden(res.Err),
|
JSON: spec.Forbidden(res.Err),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if res.Device == nil {
|
if res.Device == nil {
|
||||||
return nil, &util.JSONResponse{
|
return nil, &util.JSONResponse{
|
||||||
Code: http.StatusUnauthorized,
|
Code: http.StatusUnauthorized,
|
||||||
JSON: jsonerror.UnknownToken("Unknown token"),
|
JSON: spec.UnknownToken("Unknown token"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return res.Device, nil
|
return res.Device, nil
|
||||||
|
|
|
@ -16,6 +16,8 @@ package authtypes
|
||||||
|
|
||||||
// ThreePID represents a third-party identifier
|
// ThreePID represents a third-party identifier
|
||||||
type ThreePID struct {
|
type ThreePID struct {
|
||||||
Address string `json:"address"`
|
Address string `json:"address"`
|
||||||
Medium string `json:"medium"`
|
Medium string `json:"medium"`
|
||||||
|
AddedAt int64 `json:"added_at"`
|
||||||
|
ValidatedAt int64 `json:"validated_at"`
|
||||||
}
|
}
|
||||||
|
|
|
@ -15,15 +15,14 @@
|
||||||
package auth
|
package auth
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"io"
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
"github.com/matrix-org/dendrite/clientapi/auth/authtypes"
|
"github.com/matrix-org/dendrite/clientapi/auth/authtypes"
|
||||||
"github.com/matrix-org/dendrite/clientapi/jsonerror"
|
|
||||||
"github.com/matrix-org/dendrite/setup/config"
|
"github.com/matrix-org/dendrite/setup/config"
|
||||||
uapi "github.com/matrix-org/dendrite/userapi/api"
|
uapi "github.com/matrix-org/dendrite/userapi/api"
|
||||||
|
"github.com/matrix-org/gomatrixserverlib/spec"
|
||||||
"github.com/matrix-org/util"
|
"github.com/matrix-org/util"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -32,12 +31,17 @@ import (
|
||||||
// called after authorization has completed, with the result of the authorization.
|
// called after authorization has completed, with the result of the authorization.
|
||||||
// If the final return value is non-nil, an error occurred and the cleanup function
|
// If the final return value is non-nil, an error occurred and the cleanup function
|
||||||
// is nil.
|
// is nil.
|
||||||
func LoginFromJSONReader(ctx context.Context, r io.Reader, useraccountAPI uapi.UserLoginAPI, userAPI UserInternalAPIForLogin, cfg *config.ClientAPI) (*Login, LoginCleanupFunc, *util.JSONResponse) {
|
func LoginFromJSONReader(
|
||||||
reqBytes, err := io.ReadAll(r)
|
req *http.Request,
|
||||||
|
useraccountAPI uapi.UserLoginAPI,
|
||||||
|
userAPI UserInternalAPIForLogin,
|
||||||
|
cfg *config.ClientAPI,
|
||||||
|
) (*Login, LoginCleanupFunc, *util.JSONResponse) {
|
||||||
|
reqBytes, err := io.ReadAll(req.Body)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
err := &util.JSONResponse{
|
err := &util.JSONResponse{
|
||||||
Code: http.StatusBadRequest,
|
Code: http.StatusBadRequest,
|
||||||
JSON: jsonerror.BadJSON("Reading request body failed: " + err.Error()),
|
JSON: spec.BadJSON("Reading request body failed: " + err.Error()),
|
||||||
}
|
}
|
||||||
return nil, nil, err
|
return nil, nil, err
|
||||||
}
|
}
|
||||||
|
@ -48,7 +52,7 @@ func LoginFromJSONReader(ctx context.Context, r io.Reader, useraccountAPI uapi.U
|
||||||
if err := json.Unmarshal(reqBytes, &header); err != nil {
|
if err := json.Unmarshal(reqBytes, &header); err != nil {
|
||||||
err := &util.JSONResponse{
|
err := &util.JSONResponse{
|
||||||
Code: http.StatusBadRequest,
|
Code: http.StatusBadRequest,
|
||||||
JSON: jsonerror.BadJSON("Reading request body failed: " + err.Error()),
|
JSON: spec.BadJSON("Reading request body failed: " + err.Error()),
|
||||||
}
|
}
|
||||||
return nil, nil, err
|
return nil, nil, err
|
||||||
}
|
}
|
||||||
|
@ -57,23 +61,37 @@ func LoginFromJSONReader(ctx context.Context, r io.Reader, useraccountAPI uapi.U
|
||||||
switch header.Type {
|
switch header.Type {
|
||||||
case authtypes.LoginTypePassword:
|
case authtypes.LoginTypePassword:
|
||||||
typ = &LoginTypePassword{
|
typ = &LoginTypePassword{
|
||||||
GetAccountByPassword: useraccountAPI.QueryAccountByPassword,
|
UserAPI: useraccountAPI,
|
||||||
Config: cfg,
|
Config: cfg,
|
||||||
}
|
}
|
||||||
case authtypes.LoginTypeToken:
|
case authtypes.LoginTypeToken:
|
||||||
typ = &LoginTypeToken{
|
typ = &LoginTypeToken{
|
||||||
UserAPI: userAPI,
|
UserAPI: userAPI,
|
||||||
Config: cfg,
|
Config: cfg,
|
||||||
}
|
}
|
||||||
|
case authtypes.LoginTypeApplicationService:
|
||||||
|
token, err := ExtractAccessToken(req)
|
||||||
|
if err != nil {
|
||||||
|
err := &util.JSONResponse{
|
||||||
|
Code: http.StatusForbidden,
|
||||||
|
JSON: spec.MissingToken(err.Error()),
|
||||||
|
}
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
typ = &LoginTypeApplicationService{
|
||||||
|
Config: cfg,
|
||||||
|
Token: token,
|
||||||
|
}
|
||||||
default:
|
default:
|
||||||
err := util.JSONResponse{
|
err := util.JSONResponse{
|
||||||
Code: http.StatusBadRequest,
|
Code: http.StatusBadRequest,
|
||||||
JSON: jsonerror.InvalidArgumentValue("unhandled login type: " + header.Type),
|
JSON: spec.InvalidParam("unhandled login type: " + header.Type),
|
||||||
}
|
}
|
||||||
return nil, nil, &err
|
return nil, nil, &err
|
||||||
}
|
}
|
||||||
|
|
||||||
return typ.LoginFromJSON(ctx, reqBytes)
|
return typ.LoginFromJSON(req.Context(), reqBytes)
|
||||||
}
|
}
|
||||||
|
|
||||||
// UserInternalAPIForLogin contains the aspects of UserAPI required for logging in.
|
// UserInternalAPIForLogin contains the aspects of UserAPI required for logging in.
|
||||||
|
|
55
clientapi/auth/login_application_service.go
Normal file
55
clientapi/auth/login_application_service.go
Normal file
|
@ -0,0 +1,55 @@
|
||||||
|
// Copyright 2023 The Matrix.org Foundation C.I.C.
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
package auth
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"github.com/matrix-org/dendrite/clientapi/auth/authtypes"
|
||||||
|
"github.com/matrix-org/dendrite/clientapi/httputil"
|
||||||
|
"github.com/matrix-org/dendrite/internal"
|
||||||
|
"github.com/matrix-org/dendrite/setup/config"
|
||||||
|
"github.com/matrix-org/util"
|
||||||
|
)
|
||||||
|
|
||||||
|
// LoginTypeApplicationService describes how to authenticate as an
|
||||||
|
// application service
|
||||||
|
type LoginTypeApplicationService struct {
|
||||||
|
Config *config.ClientAPI
|
||||||
|
Token string
|
||||||
|
}
|
||||||
|
|
||||||
|
// Name implements Type
|
||||||
|
func (t *LoginTypeApplicationService) Name() string {
|
||||||
|
return authtypes.LoginTypeApplicationService
|
||||||
|
}
|
||||||
|
|
||||||
|
// LoginFromJSON implements Type
|
||||||
|
func (t *LoginTypeApplicationService) LoginFromJSON(
|
||||||
|
ctx context.Context, reqBytes []byte,
|
||||||
|
) (*Login, LoginCleanupFunc, *util.JSONResponse) {
|
||||||
|
var r Login
|
||||||
|
if err := httputil.UnmarshalJSON(reqBytes, &r); err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err := internal.ValidateApplicationServiceRequest(t.Config, r.Identifier.User, t.Token)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
cleanup := func(ctx context.Context, j *util.JSONResponse) {}
|
||||||
|
return &r, cleanup, nil
|
||||||
|
}
|
|
@ -17,15 +17,17 @@ package auth
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
"reflect"
|
"reflect"
|
||||||
|
"regexp"
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/matrix-org/dendrite/clientapi/jsonerror"
|
|
||||||
"github.com/matrix-org/dendrite/clientapi/userutil"
|
"github.com/matrix-org/dendrite/clientapi/userutil"
|
||||||
"github.com/matrix-org/dendrite/setup/config"
|
"github.com/matrix-org/dendrite/setup/config"
|
||||||
uapi "github.com/matrix-org/dendrite/userapi/api"
|
uapi "github.com/matrix-org/dendrite/userapi/api"
|
||||||
"github.com/matrix-org/gomatrixserverlib"
|
"github.com/matrix-org/gomatrixserverlib/fclient"
|
||||||
|
"github.com/matrix-org/gomatrixserverlib/spec"
|
||||||
"github.com/matrix-org/util"
|
"github.com/matrix-org/util"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -33,8 +35,9 @@ func TestLoginFromJSONReader(t *testing.T) {
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
|
|
||||||
tsts := []struct {
|
tsts := []struct {
|
||||||
Name string
|
Name string
|
||||||
Body string
|
Body string
|
||||||
|
Token string
|
||||||
|
|
||||||
WantUsername string
|
WantUsername string
|
||||||
WantDeviceID string
|
WantDeviceID string
|
||||||
|
@ -62,21 +65,69 @@ func TestLoginFromJSONReader(t *testing.T) {
|
||||||
WantDeviceID: "adevice",
|
WantDeviceID: "adevice",
|
||||||
WantDeletedTokens: []string{"atoken"},
|
WantDeletedTokens: []string{"atoken"},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
Name: "appServiceWorksUserID",
|
||||||
|
Body: `{
|
||||||
|
"type": "m.login.application_service",
|
||||||
|
"identifier": { "type": "m.id.user", "user": "@alice:example.com" },
|
||||||
|
"device_id": "adevice"
|
||||||
|
}`,
|
||||||
|
Token: "astoken",
|
||||||
|
|
||||||
|
WantUsername: "@alice:example.com",
|
||||||
|
WantDeviceID: "adevice",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "appServiceWorksLocalpart",
|
||||||
|
Body: `{
|
||||||
|
"type": "m.login.application_service",
|
||||||
|
"identifier": { "type": "m.id.user", "user": "alice" },
|
||||||
|
"device_id": "adevice"
|
||||||
|
}`,
|
||||||
|
Token: "astoken",
|
||||||
|
|
||||||
|
WantUsername: "alice",
|
||||||
|
WantDeviceID: "adevice",
|
||||||
|
},
|
||||||
}
|
}
|
||||||
for _, tst := range tsts {
|
for _, tst := range tsts {
|
||||||
t.Run(tst.Name, func(t *testing.T) {
|
t.Run(tst.Name, func(t *testing.T) {
|
||||||
var userAPI fakeUserInternalAPI
|
var userAPI fakeUserInternalAPI
|
||||||
cfg := &config.ClientAPI{
|
cfg := &config.ClientAPI{
|
||||||
Matrix: &config.Global{
|
Matrix: &config.Global{
|
||||||
SigningIdentity: gomatrixserverlib.SigningIdentity{
|
SigningIdentity: fclient.SigningIdentity{
|
||||||
ServerName: serverName,
|
ServerName: serverName,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
Derived: &config.Derived{
|
||||||
|
ApplicationServices: []config.ApplicationService{
|
||||||
|
{
|
||||||
|
ID: "anapplicationservice",
|
||||||
|
ASToken: "astoken",
|
||||||
|
NamespaceMap: map[string][]config.ApplicationServiceNamespace{
|
||||||
|
"users": {
|
||||||
|
{
|
||||||
|
Exclusive: true,
|
||||||
|
Regex: "@alice:example.com",
|
||||||
|
RegexpObject: regexp.MustCompile("@alice:example.com"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
}
|
}
|
||||||
login, cleanup, err := LoginFromJSONReader(ctx, strings.NewReader(tst.Body), &userAPI, &userAPI, cfg)
|
|
||||||
if err != nil {
|
req := httptest.NewRequest(http.MethodPost, "/", strings.NewReader(tst.Body))
|
||||||
t.Fatalf("LoginFromJSONReader failed: %+v", err)
|
if tst.Token != "" {
|
||||||
|
req.Header.Add("Authorization", "Bearer "+tst.Token)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
login, cleanup, jsonErr := LoginFromJSONReader(req, &userAPI, &userAPI, cfg)
|
||||||
|
if jsonErr != nil {
|
||||||
|
t.Fatalf("LoginFromJSONReader failed: %+v", jsonErr)
|
||||||
|
}
|
||||||
|
|
||||||
cleanup(ctx, &util.JSONResponse{Code: http.StatusOK})
|
cleanup(ctx, &util.JSONResponse{Code: http.StatusOK})
|
||||||
|
|
||||||
if login.Username() != tst.WantUsername {
|
if login.Username() != tst.WantUsername {
|
||||||
|
@ -104,16 +155,17 @@ func TestBadLoginFromJSONReader(t *testing.T) {
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
|
|
||||||
tsts := []struct {
|
tsts := []struct {
|
||||||
Name string
|
Name string
|
||||||
Body string
|
Body string
|
||||||
|
Token string
|
||||||
|
|
||||||
WantErrCode string
|
WantErrCode spec.MatrixErrorCode
|
||||||
}{
|
}{
|
||||||
{Name: "empty", WantErrCode: "M_BAD_JSON"},
|
{Name: "empty", WantErrCode: spec.ErrorBadJSON},
|
||||||
{
|
{
|
||||||
Name: "badUnmarshal",
|
Name: "badUnmarshal",
|
||||||
Body: `badsyntaxJSON`,
|
Body: `badsyntaxJSON`,
|
||||||
WantErrCode: "M_BAD_JSON",
|
WantErrCode: spec.ErrorBadJSON,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
Name: "badPassword",
|
Name: "badPassword",
|
||||||
|
@ -123,7 +175,7 @@ func TestBadLoginFromJSONReader(t *testing.T) {
|
||||||
"password": "invalidpassword",
|
"password": "invalidpassword",
|
||||||
"device_id": "adevice"
|
"device_id": "adevice"
|
||||||
}`,
|
}`,
|
||||||
WantErrCode: "M_FORBIDDEN",
|
WantErrCode: spec.ErrorForbidden,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
Name: "badToken",
|
Name: "badToken",
|
||||||
|
@ -132,7 +184,7 @@ func TestBadLoginFromJSONReader(t *testing.T) {
|
||||||
"token": "invalidtoken",
|
"token": "invalidtoken",
|
||||||
"device_id": "adevice"
|
"device_id": "adevice"
|
||||||
}`,
|
}`,
|
||||||
WantErrCode: "M_FORBIDDEN",
|
WantErrCode: spec.ErrorForbidden,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
Name: "badType",
|
Name: "badType",
|
||||||
|
@ -140,7 +192,46 @@ func TestBadLoginFromJSONReader(t *testing.T) {
|
||||||
"type": "m.login.invalid",
|
"type": "m.login.invalid",
|
||||||
"device_id": "adevice"
|
"device_id": "adevice"
|
||||||
}`,
|
}`,
|
||||||
WantErrCode: "M_INVALID_ARGUMENT_VALUE",
|
WantErrCode: spec.ErrorInvalidParam,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "noASToken",
|
||||||
|
Body: `{
|
||||||
|
"type": "m.login.application_service",
|
||||||
|
"identifier": { "type": "m.id.user", "user": "@alice:example.com" },
|
||||||
|
"device_id": "adevice"
|
||||||
|
}`,
|
||||||
|
WantErrCode: "M_MISSING_TOKEN",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "badASToken",
|
||||||
|
Token: "badastoken",
|
||||||
|
Body: `{
|
||||||
|
"type": "m.login.application_service",
|
||||||
|
"identifier": { "type": "m.id.user", "user": "@alice:example.com" },
|
||||||
|
"device_id": "adevice"
|
||||||
|
}`,
|
||||||
|
WantErrCode: "M_UNKNOWN_TOKEN",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "badASNamespace",
|
||||||
|
Token: "astoken",
|
||||||
|
Body: `{
|
||||||
|
"type": "m.login.application_service",
|
||||||
|
"identifier": { "type": "m.id.user", "user": "@bob:example.com" },
|
||||||
|
"device_id": "adevice"
|
||||||
|
}`,
|
||||||
|
WantErrCode: "M_EXCLUSIVE",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "badASUserID",
|
||||||
|
Token: "astoken",
|
||||||
|
Body: `{
|
||||||
|
"type": "m.login.application_service",
|
||||||
|
"identifier": { "type": "m.id.user", "user": "@alice:wrong.example.com" },
|
||||||
|
"device_id": "adevice"
|
||||||
|
}`,
|
||||||
|
WantErrCode: "M_INVALID_USERNAME",
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
for _, tst := range tsts {
|
for _, tst := range tsts {
|
||||||
|
@ -148,16 +239,38 @@ func TestBadLoginFromJSONReader(t *testing.T) {
|
||||||
var userAPI fakeUserInternalAPI
|
var userAPI fakeUserInternalAPI
|
||||||
cfg := &config.ClientAPI{
|
cfg := &config.ClientAPI{
|
||||||
Matrix: &config.Global{
|
Matrix: &config.Global{
|
||||||
SigningIdentity: gomatrixserverlib.SigningIdentity{
|
SigningIdentity: fclient.SigningIdentity{
|
||||||
ServerName: serverName,
|
ServerName: serverName,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
Derived: &config.Derived{
|
||||||
|
ApplicationServices: []config.ApplicationService{
|
||||||
|
{
|
||||||
|
ID: "anapplicationservice",
|
||||||
|
ASToken: "astoken",
|
||||||
|
NamespaceMap: map[string][]config.ApplicationServiceNamespace{
|
||||||
|
"users": {
|
||||||
|
{
|
||||||
|
Exclusive: true,
|
||||||
|
Regex: "@alice:example.com",
|
||||||
|
RegexpObject: regexp.MustCompile("@alice:example.com"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
}
|
}
|
||||||
_, cleanup, errRes := LoginFromJSONReader(ctx, strings.NewReader(tst.Body), &userAPI, &userAPI, cfg)
|
req := httptest.NewRequest(http.MethodPost, "/", strings.NewReader(tst.Body))
|
||||||
|
if tst.Token != "" {
|
||||||
|
req.Header.Add("Authorization", "Bearer "+tst.Token)
|
||||||
|
}
|
||||||
|
|
||||||
|
_, cleanup, errRes := LoginFromJSONReader(req, &userAPI, &userAPI, cfg)
|
||||||
if errRes == nil {
|
if errRes == nil {
|
||||||
cleanup(ctx, nil)
|
cleanup(ctx, nil)
|
||||||
t.Fatalf("LoginFromJSONReader err: got %+v, want code %q", errRes, tst.WantErrCode)
|
t.Fatalf("LoginFromJSONReader err: got %+v, want code %q", errRes, tst.WantErrCode)
|
||||||
} else if merr, ok := errRes.JSON.(*jsonerror.MatrixError); ok && merr.ErrCode != tst.WantErrCode {
|
} else if merr, ok := errRes.JSON.(spec.MatrixError); ok && merr.ErrCode != tst.WantErrCode {
|
||||||
t.Fatalf("LoginFromJSONReader err: got %+v, want code %q", errRes, tst.WantErrCode)
|
t.Fatalf("LoginFromJSONReader err: got %+v, want code %q", errRes, tst.WantErrCode)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
@ -179,6 +292,14 @@ func (ua *fakeUserInternalAPI) QueryAccountByPassword(ctx context.Context, req *
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (ua *fakeUserInternalAPI) QueryAccountByLocalpart(ctx context.Context, req *uapi.QueryAccountByLocalpartRequest, res *uapi.QueryAccountByLocalpartResponse) error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ua *fakeUserInternalAPI) PerformAccountCreation(ctx context.Context, req *uapi.PerformAccountCreationRequest, res *uapi.PerformAccountCreationResponse) error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func (ua *fakeUserInternalAPI) PerformLoginTokenDeletion(ctx context.Context, req *uapi.PerformLoginTokenDeletionRequest, res *uapi.PerformLoginTokenDeletionResponse) error {
|
func (ua *fakeUserInternalAPI) PerformLoginTokenDeletion(ctx context.Context, req *uapi.PerformLoginTokenDeletionRequest, res *uapi.PerformLoginTokenDeletionResponse) error {
|
||||||
ua.DeletedTokens = append(ua.DeletedTokens, req.Token)
|
ua.DeletedTokens = append(ua.DeletedTokens, req.Token)
|
||||||
return nil
|
return nil
|
||||||
|
|
|
@ -20,9 +20,9 @@ import (
|
||||||
|
|
||||||
"github.com/matrix-org/dendrite/clientapi/auth/authtypes"
|
"github.com/matrix-org/dendrite/clientapi/auth/authtypes"
|
||||||
"github.com/matrix-org/dendrite/clientapi/httputil"
|
"github.com/matrix-org/dendrite/clientapi/httputil"
|
||||||
"github.com/matrix-org/dendrite/clientapi/jsonerror"
|
|
||||||
"github.com/matrix-org/dendrite/setup/config"
|
"github.com/matrix-org/dendrite/setup/config"
|
||||||
uapi "github.com/matrix-org/dendrite/userapi/api"
|
uapi "github.com/matrix-org/dendrite/userapi/api"
|
||||||
|
"github.com/matrix-org/gomatrixserverlib/spec"
|
||||||
"github.com/matrix-org/util"
|
"github.com/matrix-org/util"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -48,13 +48,15 @@ func (t *LoginTypeToken) LoginFromJSON(ctx context.Context, reqBytes []byte) (*L
|
||||||
var res uapi.QueryLoginTokenResponse
|
var res uapi.QueryLoginTokenResponse
|
||||||
if err := t.UserAPI.QueryLoginToken(ctx, &uapi.QueryLoginTokenRequest{Token: r.Token}, &res); err != nil {
|
if err := t.UserAPI.QueryLoginToken(ctx, &uapi.QueryLoginTokenRequest{Token: r.Token}, &res); err != nil {
|
||||||
util.GetLogger(ctx).WithError(err).Error("UserAPI.QueryLoginToken failed")
|
util.GetLogger(ctx).WithError(err).Error("UserAPI.QueryLoginToken failed")
|
||||||
jsonErr := jsonerror.InternalServerError()
|
return nil, nil, &util.JSONResponse{
|
||||||
return nil, nil, &jsonErr
|
Code: http.StatusInternalServerError,
|
||||||
|
JSON: spec.InternalServerError{},
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if res.Data == nil {
|
if res.Data == nil {
|
||||||
return nil, nil, &util.JSONResponse{
|
return nil, nil, &util.JSONResponse{
|
||||||
Code: http.StatusForbidden,
|
Code: http.StatusForbidden,
|
||||||
JSON: jsonerror.Forbidden("invalid login token"),
|
JSON: spec.Forbidden("invalid login token"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -16,20 +16,21 @@ package auth
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"database/sql"
|
||||||
|
"github.com/go-ldap/ldap/v3"
|
||||||
|
"github.com/google/uuid"
|
||||||
"net/http"
|
"net/http"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/matrix-org/dendrite/clientapi/auth/authtypes"
|
"github.com/matrix-org/dendrite/clientapi/auth/authtypes"
|
||||||
"github.com/matrix-org/dendrite/clientapi/httputil"
|
"github.com/matrix-org/dendrite/clientapi/httputil"
|
||||||
"github.com/matrix-org/dendrite/clientapi/jsonerror"
|
|
||||||
"github.com/matrix-org/dendrite/clientapi/userutil"
|
"github.com/matrix-org/dendrite/clientapi/userutil"
|
||||||
"github.com/matrix-org/dendrite/setup/config"
|
"github.com/matrix-org/dendrite/setup/config"
|
||||||
"github.com/matrix-org/dendrite/userapi/api"
|
"github.com/matrix-org/dendrite/userapi/api"
|
||||||
|
"github.com/matrix-org/gomatrixserverlib/spec"
|
||||||
"github.com/matrix-org/util"
|
"github.com/matrix-org/util"
|
||||||
)
|
)
|
||||||
|
|
||||||
type GetAccountByPassword func(ctx context.Context, req *api.QueryAccountByPasswordRequest, res *api.QueryAccountByPasswordResponse) error
|
|
||||||
|
|
||||||
type PasswordRequest struct {
|
type PasswordRequest struct {
|
||||||
Login
|
Login
|
||||||
Password string `json:"password"`
|
Password string `json:"password"`
|
||||||
|
@ -37,8 +38,8 @@ type PasswordRequest struct {
|
||||||
|
|
||||||
// LoginTypePassword implements https://matrix.org/docs/spec/client_server/r0.6.1#password-based
|
// LoginTypePassword implements https://matrix.org/docs/spec/client_server/r0.6.1#password-based
|
||||||
type LoginTypePassword struct {
|
type LoginTypePassword struct {
|
||||||
GetAccountByPassword GetAccountByPassword
|
Config *config.ClientAPI
|
||||||
Config *config.ClientAPI
|
UserAPI api.UserLoginAPI
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t *LoginTypePassword) Name() string {
|
func (t *LoginTypePassword) Name() string {
|
||||||
|
@ -59,73 +60,227 @@ func (t *LoginTypePassword) LoginFromJSON(ctx context.Context, reqBytes []byte)
|
||||||
return login, func(context.Context, *util.JSONResponse) {}, nil
|
return login, func(context.Context, *util.JSONResponse) {}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t *LoginTypePassword) Login(ctx context.Context, req interface{}) (*Login, *util.JSONResponse) {
|
func (t *LoginTypePassword) Login(ctx context.Context, request *PasswordRequest) (*Login, *util.JSONResponse) {
|
||||||
r := req.(*PasswordRequest)
|
fullUsername := request.Username()
|
||||||
username := r.Username()
|
if fullUsername == "" {
|
||||||
if username == "" {
|
|
||||||
return nil, &util.JSONResponse{
|
return nil, &util.JSONResponse{
|
||||||
Code: http.StatusUnauthorized,
|
Code: http.StatusUnauthorized,
|
||||||
JSON: jsonerror.BadJSON("A username must be supplied."),
|
JSON: spec.BadJSON("A username must be supplied."),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if len(r.Password) == 0 {
|
if len(request.Password) == 0 {
|
||||||
return nil, &util.JSONResponse{
|
return nil, &util.JSONResponse{
|
||||||
Code: http.StatusUnauthorized,
|
Code: http.StatusUnauthorized,
|
||||||
JSON: jsonerror.BadJSON("A password must be supplied."),
|
JSON: spec.BadJSON("A password must be supplied."),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
localpart, domain, err := userutil.ParseUsernameParam(username, t.Config.Matrix)
|
username, domain, err := userutil.ParseUsernameParam(fullUsername, t.Config.Matrix)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, &util.JSONResponse{
|
return nil, &util.JSONResponse{
|
||||||
Code: http.StatusUnauthorized,
|
Code: http.StatusUnauthorized,
|
||||||
JSON: jsonerror.InvalidUsername(err.Error()),
|
JSON: spec.InvalidUsername(err.Error()),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if !t.Config.Matrix.IsLocalServerName(domain) {
|
if !t.Config.Matrix.IsLocalServerName(domain) {
|
||||||
return nil, &util.JSONResponse{
|
return nil, &util.JSONResponse{
|
||||||
Code: http.StatusUnauthorized,
|
Code: http.StatusUnauthorized,
|
||||||
JSON: jsonerror.InvalidUsername("The server name is not known."),
|
JSON: spec.InvalidUsername("The server name is not known."),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Squash username to all lowercase letters
|
|
||||||
|
var account *api.Account
|
||||||
|
if t.Config.Ldap.Enabled {
|
||||||
|
isAdmin, err := t.authenticateLdap(username, request.Password)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
acc, err := t.getOrCreateAccount(ctx, username, domain, isAdmin)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
account = acc
|
||||||
|
} else {
|
||||||
|
acc, err := t.authenticateDb(ctx, username, domain, request.Password)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
account = acc
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set the user, so login.Username() can do the right thing
|
||||||
|
request.Identifier.User = account.UserID
|
||||||
|
request.User = account.UserID
|
||||||
|
return &request.Login, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *LoginTypePassword) authenticateDb(ctx context.Context, username string, domain spec.ServerName, password string) (*api.Account, *util.JSONResponse) {
|
||||||
res := &api.QueryAccountByPasswordResponse{}
|
res := &api.QueryAccountByPasswordResponse{}
|
||||||
err = t.GetAccountByPassword(ctx, &api.QueryAccountByPasswordRequest{
|
err := t.UserAPI.QueryAccountByPassword(ctx, &api.QueryAccountByPasswordRequest{
|
||||||
Localpart: strings.ToLower(localpart),
|
Localpart: strings.ToLower(username),
|
||||||
ServerName: domain,
|
ServerName: domain,
|
||||||
PlaintextPassword: r.Password,
|
PlaintextPassword: password,
|
||||||
}, res)
|
}, res)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, &util.JSONResponse{
|
return nil, &util.JSONResponse{
|
||||||
Code: http.StatusInternalServerError,
|
Code: http.StatusInternalServerError,
|
||||||
JSON: jsonerror.Unknown("Unable to fetch account by password."),
|
JSON: spec.Unknown("Unable to fetch account by password."),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// If we couldn't find the user by the lower cased localpart, try the provided
|
|
||||||
// localpart as is.
|
|
||||||
if !res.Exists {
|
if !res.Exists {
|
||||||
err = t.GetAccountByPassword(ctx, &api.QueryAccountByPasswordRequest{
|
err = t.UserAPI.QueryAccountByPassword(ctx, &api.QueryAccountByPasswordRequest{
|
||||||
Localpart: localpart,
|
Localpart: username,
|
||||||
ServerName: domain,
|
ServerName: domain,
|
||||||
PlaintextPassword: r.Password,
|
PlaintextPassword: password,
|
||||||
}, res)
|
}, res)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, &util.JSONResponse{
|
return nil, &util.JSONResponse{
|
||||||
Code: http.StatusInternalServerError,
|
Code: http.StatusInternalServerError,
|
||||||
JSON: jsonerror.Unknown("Unable to fetch account by password."),
|
JSON: spec.Unknown("Unable to fetch account by password."),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Technically we could tell them if the user does not exist by checking if err == sql.ErrNoRows
|
|
||||||
// but that would leak the existence of the user.
|
|
||||||
if !res.Exists {
|
if !res.Exists {
|
||||||
return nil, &util.JSONResponse{
|
return nil, &util.JSONResponse{
|
||||||
Code: http.StatusForbidden,
|
Code: http.StatusForbidden,
|
||||||
JSON: jsonerror.Forbidden("The username or password was incorrect or the account does not exist."),
|
JSON: spec.Forbidden("The username or password was incorrect or the account does not exist."),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Set the user, so login.Username() can do the right thing
|
return res.Account, nil
|
||||||
r.Identifier.User = res.Account.UserID
|
}
|
||||||
r.User = res.Account.UserID
|
func (t *LoginTypePassword) authenticateLdap(username, password string) (bool, *util.JSONResponse) {
|
||||||
return &r.Login, nil
|
var conn *ldap.Conn
|
||||||
|
conn, err := ldap.DialURL(t.Config.Ldap.Uri)
|
||||||
|
if err != nil {
|
||||||
|
return false, &util.JSONResponse{
|
||||||
|
Code: http.StatusInternalServerError,
|
||||||
|
JSON: spec.Unknown("unable to connect to ldap: " + err.Error()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
defer conn.Close()
|
||||||
|
|
||||||
|
if t.Config.Ldap.AdminBindEnabled {
|
||||||
|
err = conn.Bind(t.Config.Ldap.AdminBindDn, t.Config.Ldap.AdminBindPassword)
|
||||||
|
if err != nil {
|
||||||
|
return false, &util.JSONResponse{
|
||||||
|
Code: http.StatusInternalServerError,
|
||||||
|
JSON: spec.Unknown("unable to bind to ldap: " + err.Error()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
filter := strings.ReplaceAll(t.Config.Ldap.SearchFilter, "{username}", username)
|
||||||
|
searchRequest := ldap.NewSearchRequest(
|
||||||
|
t.Config.Ldap.BaseDn, ldap.ScopeWholeSubtree, ldap.NeverDerefAliases,
|
||||||
|
0, 0, false, filter, []string{t.Config.Ldap.SearchAttribute}, nil,
|
||||||
|
)
|
||||||
|
result, err := conn.Search(searchRequest)
|
||||||
|
if err != nil {
|
||||||
|
return false, &util.JSONResponse{
|
||||||
|
Code: http.StatusInternalServerError,
|
||||||
|
JSON: spec.Unknown("unable to bind to search ldap: " + err.Error()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(result.Entries) > 1 {
|
||||||
|
return false, &util.JSONResponse{
|
||||||
|
Code: http.StatusUnauthorized,
|
||||||
|
JSON: spec.BadJSON("'user' must be duplicated."),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(result.Entries) < 1 {
|
||||||
|
return false, &util.JSONResponse{
|
||||||
|
Code: http.StatusUnauthorized,
|
||||||
|
JSON: spec.BadJSON("'user' not found."),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
userDN := result.Entries[0].DN
|
||||||
|
err = conn.Bind(userDN, password)
|
||||||
|
if err != nil {
|
||||||
|
return false, &util.JSONResponse{
|
||||||
|
Code: http.StatusUnauthorized,
|
||||||
|
JSON: spec.InvalidUsername(err.Error()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
bindDn := strings.ReplaceAll(t.Config.Ldap.UserBindDn, "{username}", username)
|
||||||
|
err = conn.Bind(bindDn, password)
|
||||||
|
if err != nil {
|
||||||
|
return false, &util.JSONResponse{
|
||||||
|
Code: http.StatusUnauthorized,
|
||||||
|
JSON: spec.InvalidUsername(err.Error()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
isAdmin, err := t.isLdapAdmin(conn, username)
|
||||||
|
if err != nil {
|
||||||
|
return false, &util.JSONResponse{
|
||||||
|
Code: http.StatusUnauthorized,
|
||||||
|
JSON: spec.InvalidUsername(err.Error()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return isAdmin, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *LoginTypePassword) isLdapAdmin(conn *ldap.Conn, username string) (bool, error) {
|
||||||
|
searchRequest := ldap.NewSearchRequest(
|
||||||
|
t.Config.Ldap.AdminGroupDn,
|
||||||
|
ldap.ScopeWholeSubtree, ldap.DerefAlways, 0, 0, false,
|
||||||
|
strings.ReplaceAll(t.Config.Ldap.AdminGroupFilter, "{username}", username),
|
||||||
|
[]string{t.Config.Ldap.AdminGroupAttribute},
|
||||||
|
nil)
|
||||||
|
|
||||||
|
sr, err := conn.Search(searchRequest)
|
||||||
|
if err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(sr.Entries) < 1 {
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
return true, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *LoginTypePassword) getOrCreateAccount(ctx context.Context, username string, domain spec.ServerName, admin bool) (*api.Account, *util.JSONResponse) {
|
||||||
|
var existing api.QueryAccountByLocalpartResponse
|
||||||
|
err := t.UserAPI.QueryAccountByLocalpart(ctx, &api.QueryAccountByLocalpartRequest{
|
||||||
|
Localpart: username,
|
||||||
|
ServerName: domain,
|
||||||
|
}, &existing)
|
||||||
|
|
||||||
|
if err == nil {
|
||||||
|
return existing.Account, nil
|
||||||
|
}
|
||||||
|
if err != sql.ErrNoRows {
|
||||||
|
return nil, &util.JSONResponse{
|
||||||
|
Code: http.StatusUnauthorized,
|
||||||
|
JSON: spec.InvalidUsername(err.Error()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
accountType := api.AccountTypeUser
|
||||||
|
if admin {
|
||||||
|
accountType = api.AccountTypeAdmin
|
||||||
|
}
|
||||||
|
var created api.PerformAccountCreationResponse
|
||||||
|
err = t.UserAPI.PerformAccountCreation(ctx, &api.PerformAccountCreationRequest{
|
||||||
|
AppServiceID: "ldap",
|
||||||
|
Localpart: username,
|
||||||
|
Password: uuid.New().String(),
|
||||||
|
AccountType: accountType,
|
||||||
|
OnConflict: api.ConflictAbort,
|
||||||
|
}, &created)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
if _, ok := err.(*api.ErrorConflict); ok {
|
||||||
|
return nil, &util.JSONResponse{
|
||||||
|
Code: http.StatusBadRequest,
|
||||||
|
JSON: spec.UserInUse("Desired user ID is already taken."),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil, &util.JSONResponse{
|
||||||
|
Code: http.StatusInternalServerError,
|
||||||
|
JSON: spec.Unknown("failed to create account: " + err.Error()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return created.Account, nil
|
||||||
}
|
}
|
||||||
|
|
|
@ -20,9 +20,9 @@ import (
|
||||||
"net/http"
|
"net/http"
|
||||||
"sync"
|
"sync"
|
||||||
|
|
||||||
"github.com/matrix-org/dendrite/clientapi/jsonerror"
|
|
||||||
"github.com/matrix-org/dendrite/setup/config"
|
"github.com/matrix-org/dendrite/setup/config"
|
||||||
"github.com/matrix-org/dendrite/userapi/api"
|
"github.com/matrix-org/dendrite/userapi/api"
|
||||||
|
"github.com/matrix-org/gomatrixserverlib/spec"
|
||||||
"github.com/matrix-org/util"
|
"github.com/matrix-org/util"
|
||||||
"github.com/sirupsen/logrus"
|
"github.com/sirupsen/logrus"
|
||||||
"github.com/tidwall/gjson"
|
"github.com/tidwall/gjson"
|
||||||
|
@ -55,7 +55,7 @@ type LoginCleanupFunc func(context.Context, *util.JSONResponse)
|
||||||
// https://matrix.org/docs/spec/client_server/r0.6.1#identifier-types
|
// https://matrix.org/docs/spec/client_server/r0.6.1#identifier-types
|
||||||
type LoginIdentifier struct {
|
type LoginIdentifier struct {
|
||||||
Type string `json:"type"`
|
Type string `json:"type"`
|
||||||
// when type = m.id.user
|
// when type = m.id.user or m.id.application_service
|
||||||
User string `json:"user"`
|
User string `json:"user"`
|
||||||
// when type = m.id.thirdparty
|
// when type = m.id.thirdparty
|
||||||
Medium string `json:"medium"`
|
Medium string `json:"medium"`
|
||||||
|
@ -113,8 +113,8 @@ type UserInteractive struct {
|
||||||
|
|
||||||
func NewUserInteractive(userAccountAPI api.UserLoginAPI, cfg *config.ClientAPI) *UserInteractive {
|
func NewUserInteractive(userAccountAPI api.UserLoginAPI, cfg *config.ClientAPI) *UserInteractive {
|
||||||
typePassword := &LoginTypePassword{
|
typePassword := &LoginTypePassword{
|
||||||
GetAccountByPassword: userAccountAPI.QueryAccountByPassword,
|
UserAPI: userAccountAPI,
|
||||||
Config: cfg,
|
Config: cfg,
|
||||||
}
|
}
|
||||||
return &UserInteractive{
|
return &UserInteractive{
|
||||||
Flows: []userInteractiveFlow{
|
Flows: []userInteractiveFlow{
|
||||||
|
@ -178,8 +178,10 @@ func (u *UserInteractive) NewSession() *util.JSONResponse {
|
||||||
sessionID, err := GenerateAccessToken()
|
sessionID, err := GenerateAccessToken()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logrus.WithError(err).Error("failed to generate session ID")
|
logrus.WithError(err).Error("failed to generate session ID")
|
||||||
res := jsonerror.InternalServerError()
|
return &util.JSONResponse{
|
||||||
return &res
|
Code: http.StatusInternalServerError,
|
||||||
|
JSON: spec.InternalServerError{},
|
||||||
|
}
|
||||||
}
|
}
|
||||||
u.Lock()
|
u.Lock()
|
||||||
u.Sessions[sessionID] = []string{}
|
u.Sessions[sessionID] = []string{}
|
||||||
|
@ -193,15 +195,19 @@ func (u *UserInteractive) ResponseWithChallenge(sessionID string, response inter
|
||||||
mixedObjects := make(map[string]interface{})
|
mixedObjects := make(map[string]interface{})
|
||||||
b, err := json.Marshal(response)
|
b, err := json.Marshal(response)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
ise := jsonerror.InternalServerError()
|
return &util.JSONResponse{
|
||||||
return &ise
|
Code: http.StatusInternalServerError,
|
||||||
|
JSON: spec.InternalServerError{},
|
||||||
|
}
|
||||||
}
|
}
|
||||||
_ = json.Unmarshal(b, &mixedObjects)
|
_ = json.Unmarshal(b, &mixedObjects)
|
||||||
challenge := u.challenge(sessionID)
|
challenge := u.challenge(sessionID)
|
||||||
b, err = json.Marshal(challenge.JSON)
|
b, err = json.Marshal(challenge.JSON)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
ise := jsonerror.InternalServerError()
|
return &util.JSONResponse{
|
||||||
return &ise
|
Code: http.StatusInternalServerError,
|
||||||
|
JSON: spec.InternalServerError{},
|
||||||
|
}
|
||||||
}
|
}
|
||||||
_ = json.Unmarshal(b, &mixedObjects)
|
_ = json.Unmarshal(b, &mixedObjects)
|
||||||
|
|
||||||
|
@ -234,7 +240,7 @@ func (u *UserInteractive) Verify(ctx context.Context, bodyBytes []byte, device *
|
||||||
if !ok {
|
if !ok {
|
||||||
return nil, &util.JSONResponse{
|
return nil, &util.JSONResponse{
|
||||||
Code: http.StatusBadRequest,
|
Code: http.StatusBadRequest,
|
||||||
JSON: jsonerror.BadJSON("Unknown auth.type: " + authType),
|
JSON: spec.BadJSON("Unknown auth.type: " + authType),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -250,7 +256,7 @@ func (u *UserInteractive) Verify(ctx context.Context, bodyBytes []byte, device *
|
||||||
if !u.IsSingleStageFlow(authType) {
|
if !u.IsSingleStageFlow(authType) {
|
||||||
return nil, &util.JSONResponse{
|
return nil, &util.JSONResponse{
|
||||||
Code: http.StatusBadRequest,
|
Code: http.StatusBadRequest,
|
||||||
JSON: jsonerror.Unknown("The auth.session is missing or unknown."),
|
JSON: spec.Unknown("The auth.session is missing or unknown."),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,13 +8,14 @@ import (
|
||||||
|
|
||||||
"github.com/matrix-org/dendrite/setup/config"
|
"github.com/matrix-org/dendrite/setup/config"
|
||||||
"github.com/matrix-org/dendrite/userapi/api"
|
"github.com/matrix-org/dendrite/userapi/api"
|
||||||
"github.com/matrix-org/gomatrixserverlib"
|
"github.com/matrix-org/gomatrixserverlib/fclient"
|
||||||
|
"github.com/matrix-org/gomatrixserverlib/spec"
|
||||||
"github.com/matrix-org/util"
|
"github.com/matrix-org/util"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
ctx = context.Background()
|
ctx = context.Background()
|
||||||
serverName = gomatrixserverlib.ServerName("example.com")
|
serverName = spec.ServerName("example.com")
|
||||||
// space separated localpart+password -> account
|
// space separated localpart+password -> account
|
||||||
lookup = make(map[string]*api.Account)
|
lookup = make(map[string]*api.Account)
|
||||||
device = &api.Device{
|
device = &api.Device{
|
||||||
|
@ -44,10 +45,18 @@ func (d *fakeAccountDatabase) QueryAccountByPassword(ctx context.Context, req *a
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (d *fakeAccountDatabase) QueryAccountByLocalpart(ctx context.Context, req *api.QueryAccountByLocalpartRequest, res *api.QueryAccountByLocalpartResponse) error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *fakeAccountDatabase) PerformAccountCreation(ctx context.Context, req *api.PerformAccountCreationRequest, res *api.PerformAccountCreationResponse) error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func setup() *UserInteractive {
|
func setup() *UserInteractive {
|
||||||
cfg := &config.ClientAPI{
|
cfg := &config.ClientAPI{
|
||||||
Matrix: &config.Global{
|
Matrix: &config.Global{
|
||||||
SigningIdentity: gomatrixserverlib.SigningIdentity{
|
SigningIdentity: fclient.SigningIdentity{
|
||||||
ServerName: serverName,
|
ServerName: serverName,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
|
@ -15,8 +15,11 @@
|
||||||
package clientapi
|
package clientapi
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"github.com/matrix-org/dendrite/internal/httputil"
|
||||||
|
"github.com/matrix-org/dendrite/setup/config"
|
||||||
|
"github.com/matrix-org/dendrite/setup/process"
|
||||||
userapi "github.com/matrix-org/dendrite/userapi/api"
|
userapi "github.com/matrix-org/dendrite/userapi/api"
|
||||||
"github.com/matrix-org/gomatrixserverlib"
|
"github.com/matrix-org/gomatrixserverlib/fclient"
|
||||||
|
|
||||||
appserviceAPI "github.com/matrix-org/dendrite/appservice/api"
|
appserviceAPI "github.com/matrix-org/dendrite/appservice/api"
|
||||||
"github.com/matrix-org/dendrite/clientapi/api"
|
"github.com/matrix-org/dendrite/clientapi/api"
|
||||||
|
@ -25,41 +28,41 @@ import (
|
||||||
federationAPI "github.com/matrix-org/dendrite/federationapi/api"
|
federationAPI "github.com/matrix-org/dendrite/federationapi/api"
|
||||||
"github.com/matrix-org/dendrite/internal/transactions"
|
"github.com/matrix-org/dendrite/internal/transactions"
|
||||||
roomserverAPI "github.com/matrix-org/dendrite/roomserver/api"
|
roomserverAPI "github.com/matrix-org/dendrite/roomserver/api"
|
||||||
"github.com/matrix-org/dendrite/setup/base"
|
|
||||||
"github.com/matrix-org/dendrite/setup/jetstream"
|
"github.com/matrix-org/dendrite/setup/jetstream"
|
||||||
)
|
)
|
||||||
|
|
||||||
// AddPublicRoutes sets up and registers HTTP handlers for the ClientAPI component.
|
// AddPublicRoutes sets up and registers HTTP handlers for the ClientAPI component.
|
||||||
func AddPublicRoutes(
|
func AddPublicRoutes(
|
||||||
base *base.BaseDendrite,
|
processContext *process.ProcessContext,
|
||||||
federation *gomatrixserverlib.FederationClient,
|
routers httputil.Routers,
|
||||||
|
cfg *config.Dendrite,
|
||||||
|
natsInstance *jetstream.NATSInstance,
|
||||||
|
federation fclient.FederationClient,
|
||||||
rsAPI roomserverAPI.ClientRoomserverAPI,
|
rsAPI roomserverAPI.ClientRoomserverAPI,
|
||||||
asAPI appserviceAPI.AppServiceInternalAPI,
|
asAPI appserviceAPI.AppServiceInternalAPI,
|
||||||
transactionsCache *transactions.Cache,
|
transactionsCache *transactions.Cache,
|
||||||
fsAPI federationAPI.ClientFederationAPI,
|
fsAPI federationAPI.ClientFederationAPI,
|
||||||
userAPI userapi.ClientUserAPI,
|
userAPI userapi.ClientUserAPI,
|
||||||
userDirectoryProvider userapi.QuerySearchProfilesAPI,
|
userDirectoryProvider userapi.QuerySearchProfilesAPI,
|
||||||
extRoomsProvider api.ExtraPublicRoomsProvider,
|
extRoomsProvider api.ExtraPublicRoomsProvider, enableMetrics bool,
|
||||||
) {
|
) {
|
||||||
cfg := &base.Cfg.ClientAPI
|
js, natsClient := natsInstance.Prepare(processContext, &cfg.Global.JetStream)
|
||||||
mscCfg := &base.Cfg.MSCs
|
|
||||||
js, natsClient := base.NATS.Prepare(base.ProcessContext, &cfg.Matrix.JetStream)
|
|
||||||
|
|
||||||
syncProducer := &producers.SyncAPIProducer{
|
syncProducer := &producers.SyncAPIProducer{
|
||||||
JetStream: js,
|
JetStream: js,
|
||||||
TopicReceiptEvent: cfg.Matrix.JetStream.Prefixed(jetstream.OutputReceiptEvent),
|
TopicReceiptEvent: cfg.Global.JetStream.Prefixed(jetstream.OutputReceiptEvent),
|
||||||
TopicSendToDeviceEvent: cfg.Matrix.JetStream.Prefixed(jetstream.OutputSendToDeviceEvent),
|
TopicSendToDeviceEvent: cfg.Global.JetStream.Prefixed(jetstream.OutputSendToDeviceEvent),
|
||||||
TopicTypingEvent: cfg.Matrix.JetStream.Prefixed(jetstream.OutputTypingEvent),
|
TopicTypingEvent: cfg.Global.JetStream.Prefixed(jetstream.OutputTypingEvent),
|
||||||
TopicPresenceEvent: cfg.Matrix.JetStream.Prefixed(jetstream.OutputPresenceEvent),
|
TopicPresenceEvent: cfg.Global.JetStream.Prefixed(jetstream.OutputPresenceEvent),
|
||||||
UserAPI: userAPI,
|
UserAPI: userAPI,
|
||||||
ServerName: cfg.Matrix.ServerName,
|
ServerName: cfg.Global.ServerName,
|
||||||
}
|
}
|
||||||
|
|
||||||
routing.Setup(
|
routing.Setup(
|
||||||
base,
|
routers,
|
||||||
cfg, rsAPI, asAPI,
|
cfg, rsAPI, asAPI,
|
||||||
userAPI, userDirectoryProvider, federation,
|
userAPI, userDirectoryProvider, federation,
|
||||||
syncProducer, transactionsCache, fsAPI,
|
syncProducer, transactionsCache, fsAPI,
|
||||||
extRoomsProvider, mscCfg, natsClient,
|
extRoomsProvider, natsClient, enableMetrics,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
2437
clientapi/clientapi_test.go
Normal file
2437
clientapi/clientapi_test.go
Normal file
File diff suppressed because it is too large
Load diff
|
@ -20,7 +20,7 @@ import (
|
||||||
"net/http"
|
"net/http"
|
||||||
"unicode/utf8"
|
"unicode/utf8"
|
||||||
|
|
||||||
"github.com/matrix-org/dendrite/clientapi/jsonerror"
|
"github.com/matrix-org/gomatrixserverlib/spec"
|
||||||
"github.com/matrix-org/util"
|
"github.com/matrix-org/util"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -32,8 +32,10 @@ func UnmarshalJSONRequest(req *http.Request, iface interface{}) *util.JSONRespon
|
||||||
body, err := io.ReadAll(req.Body)
|
body, err := io.ReadAll(req.Body)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
util.GetLogger(req.Context()).WithError(err).Error("io.ReadAll failed")
|
util.GetLogger(req.Context()).WithError(err).Error("io.ReadAll failed")
|
||||||
resp := jsonerror.InternalServerError()
|
return &util.JSONResponse{
|
||||||
return &resp
|
Code: http.StatusInternalServerError,
|
||||||
|
JSON: spec.InternalServerError{},
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return UnmarshalJSON(body, iface)
|
return UnmarshalJSON(body, iface)
|
||||||
|
@ -43,7 +45,7 @@ func UnmarshalJSON(body []byte, iface interface{}) *util.JSONResponse {
|
||||||
if !utf8.Valid(body) {
|
if !utf8.Valid(body) {
|
||||||
return &util.JSONResponse{
|
return &util.JSONResponse{
|
||||||
Code: http.StatusBadRequest,
|
Code: http.StatusBadRequest,
|
||||||
JSON: jsonerror.NotJSON("Body contains invalid UTF-8"),
|
JSON: spec.NotJSON("Body contains invalid UTF-8"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -53,7 +55,7 @@ func UnmarshalJSON(body []byte, iface interface{}) *util.JSONResponse {
|
||||||
// valid JSON with incorrect types for values.
|
// valid JSON with incorrect types for values.
|
||||||
return &util.JSONResponse{
|
return &util.JSONResponse{
|
||||||
Code: http.StatusBadRequest,
|
Code: http.StatusBadRequest,
|
||||||
JSON: jsonerror.BadJSON("The request body could not be decoded into valid JSON. " + err.Error()),
|
JSON: spec.BadJSON("The request body could not be decoded into valid JSON. " + err.Error()),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
|
|
|
@ -1,229 +0,0 @@
|
||||||
// Copyright 2017 Vector Creations Ltd
|
|
||||||
//
|
|
||||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
// you may not use this file except in compliance with the License.
|
|
||||||
// You may obtain a copy of the License at
|
|
||||||
//
|
|
||||||
// http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
//
|
|
||||||
// Unless required by applicable law or agreed to in writing, software
|
|
||||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
// See the License for the specific language governing permissions and
|
|
||||||
// limitations under the License.
|
|
||||||
|
|
||||||
package jsonerror
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"fmt"
|
|
||||||
"net/http"
|
|
||||||
|
|
||||||
"github.com/matrix-org/gomatrixserverlib"
|
|
||||||
"github.com/matrix-org/util"
|
|
||||||
"github.com/sirupsen/logrus"
|
|
||||||
)
|
|
||||||
|
|
||||||
// MatrixError represents the "standard error response" in Matrix.
|
|
||||||
// http://matrix.org/docs/spec/client_server/r0.2.0.html#api-standards
|
|
||||||
type MatrixError struct {
|
|
||||||
ErrCode string `json:"errcode"`
|
|
||||||
Err string `json:"error"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func (e MatrixError) Error() string {
|
|
||||||
return fmt.Sprintf("%s: %s", e.ErrCode, e.Err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// InternalServerError returns a 500 Internal Server Error in a matrix-compliant
|
|
||||||
// format.
|
|
||||||
func InternalServerError() util.JSONResponse {
|
|
||||||
return util.JSONResponse{
|
|
||||||
Code: http.StatusInternalServerError,
|
|
||||||
JSON: Unknown("Internal Server Error"),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Unknown is an unexpected error
|
|
||||||
func Unknown(msg string) *MatrixError {
|
|
||||||
return &MatrixError{"M_UNKNOWN", msg}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Forbidden is an error when the client tries to access a resource
|
|
||||||
// they are not allowed to access.
|
|
||||||
func Forbidden(msg string) *MatrixError {
|
|
||||||
return &MatrixError{"M_FORBIDDEN", msg}
|
|
||||||
}
|
|
||||||
|
|
||||||
// BadJSON is an error when the client supplies malformed JSON.
|
|
||||||
func BadJSON(msg string) *MatrixError {
|
|
||||||
return &MatrixError{"M_BAD_JSON", msg}
|
|
||||||
}
|
|
||||||
|
|
||||||
// BadAlias is an error when the client supplies a bad alias.
|
|
||||||
func BadAlias(msg string) *MatrixError {
|
|
||||||
return &MatrixError{"M_BAD_ALIAS", msg}
|
|
||||||
}
|
|
||||||
|
|
||||||
// NotJSON is an error when the client supplies something that is not JSON
|
|
||||||
// to a JSON endpoint.
|
|
||||||
func NotJSON(msg string) *MatrixError {
|
|
||||||
return &MatrixError{"M_NOT_JSON", msg}
|
|
||||||
}
|
|
||||||
|
|
||||||
// NotFound is an error when the client tries to access an unknown resource.
|
|
||||||
func NotFound(msg string) *MatrixError {
|
|
||||||
return &MatrixError{"M_NOT_FOUND", msg}
|
|
||||||
}
|
|
||||||
|
|
||||||
// MissingArgument is an error when the client tries to access a resource
|
|
||||||
// without providing an argument that is required.
|
|
||||||
func MissingArgument(msg string) *MatrixError {
|
|
||||||
return &MatrixError{"M_MISSING_ARGUMENT", msg}
|
|
||||||
}
|
|
||||||
|
|
||||||
// InvalidArgumentValue is an error when the client tries to provide an
|
|
||||||
// invalid value for a valid argument
|
|
||||||
func InvalidArgumentValue(msg string) *MatrixError {
|
|
||||||
return &MatrixError{"M_INVALID_ARGUMENT_VALUE", msg}
|
|
||||||
}
|
|
||||||
|
|
||||||
// MissingToken is an error when the client tries to access a resource which
|
|
||||||
// requires authentication without supplying credentials.
|
|
||||||
func MissingToken(msg string) *MatrixError {
|
|
||||||
return &MatrixError{"M_MISSING_TOKEN", msg}
|
|
||||||
}
|
|
||||||
|
|
||||||
// UnknownToken is an error when the client tries to access a resource which
|
|
||||||
// requires authentication and supplies an unrecognised token
|
|
||||||
func UnknownToken(msg string) *MatrixError {
|
|
||||||
return &MatrixError{"M_UNKNOWN_TOKEN", msg}
|
|
||||||
}
|
|
||||||
|
|
||||||
// WeakPassword is an error which is returned when the client tries to register
|
|
||||||
// using a weak password. http://matrix.org/docs/spec/client_server/r0.2.0.html#password-based
|
|
||||||
func WeakPassword(msg string) *MatrixError {
|
|
||||||
return &MatrixError{"M_WEAK_PASSWORD", msg}
|
|
||||||
}
|
|
||||||
|
|
||||||
// InvalidUsername is an error returned when the client tries to register an
|
|
||||||
// invalid username
|
|
||||||
func InvalidUsername(msg string) *MatrixError {
|
|
||||||
return &MatrixError{"M_INVALID_USERNAME", msg}
|
|
||||||
}
|
|
||||||
|
|
||||||
// UserInUse is an error returned when the client tries to register an
|
|
||||||
// username that already exists
|
|
||||||
func UserInUse(msg string) *MatrixError {
|
|
||||||
return &MatrixError{"M_USER_IN_USE", msg}
|
|
||||||
}
|
|
||||||
|
|
||||||
// RoomInUse is an error returned when the client tries to make a room
|
|
||||||
// that already exists
|
|
||||||
func RoomInUse(msg string) *MatrixError {
|
|
||||||
return &MatrixError{"M_ROOM_IN_USE", msg}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ASExclusive is an error returned when an application service tries to
|
|
||||||
// register an username that is outside of its registered namespace, or if a
|
|
||||||
// user attempts to register a username or room alias within an exclusive
|
|
||||||
// namespace.
|
|
||||||
func ASExclusive(msg string) *MatrixError {
|
|
||||||
return &MatrixError{"M_EXCLUSIVE", msg}
|
|
||||||
}
|
|
||||||
|
|
||||||
// GuestAccessForbidden is an error which is returned when the client is
|
|
||||||
// forbidden from accessing a resource as a guest.
|
|
||||||
func GuestAccessForbidden(msg string) *MatrixError {
|
|
||||||
return &MatrixError{"M_GUEST_ACCESS_FORBIDDEN", msg}
|
|
||||||
}
|
|
||||||
|
|
||||||
// InvalidSignature is an error which is returned when the client tries
|
|
||||||
// to upload invalid signatures.
|
|
||||||
func InvalidSignature(msg string) *MatrixError {
|
|
||||||
return &MatrixError{"M_INVALID_SIGNATURE", msg}
|
|
||||||
}
|
|
||||||
|
|
||||||
// InvalidParam is an error that is returned when a parameter was invalid,
|
|
||||||
// traditionally with cross-signing.
|
|
||||||
func InvalidParam(msg string) *MatrixError {
|
|
||||||
return &MatrixError{"M_INVALID_PARAM", msg}
|
|
||||||
}
|
|
||||||
|
|
||||||
// MissingParam is an error that is returned when a parameter was incorrect,
|
|
||||||
// traditionally with cross-signing.
|
|
||||||
func MissingParam(msg string) *MatrixError {
|
|
||||||
return &MatrixError{"M_MISSING_PARAM", msg}
|
|
||||||
}
|
|
||||||
|
|
||||||
// UnableToAuthoriseJoin is an error that is returned when a server can't
|
|
||||||
// determine whether to allow a restricted join or not.
|
|
||||||
func UnableToAuthoriseJoin(msg string) *MatrixError {
|
|
||||||
return &MatrixError{"M_UNABLE_TO_AUTHORISE_JOIN", msg}
|
|
||||||
}
|
|
||||||
|
|
||||||
// LeaveServerNoticeError is an error returned when trying to reject an invite
|
|
||||||
// for a server notice room.
|
|
||||||
func LeaveServerNoticeError() *MatrixError {
|
|
||||||
return &MatrixError{
|
|
||||||
ErrCode: "M_CANNOT_LEAVE_SERVER_NOTICE_ROOM",
|
|
||||||
Err: "You cannot reject this invite",
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
type IncompatibleRoomVersionError struct {
|
|
||||||
RoomVersion string `json:"room_version"`
|
|
||||||
Error string `json:"error"`
|
|
||||||
Code string `json:"errcode"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// IncompatibleRoomVersion is an error which is returned when the client
|
|
||||||
// requests a room with a version that is unsupported.
|
|
||||||
func IncompatibleRoomVersion(roomVersion gomatrixserverlib.RoomVersion) *IncompatibleRoomVersionError {
|
|
||||||
return &IncompatibleRoomVersionError{
|
|
||||||
Code: "M_INCOMPATIBLE_ROOM_VERSION",
|
|
||||||
RoomVersion: string(roomVersion),
|
|
||||||
Error: "Your homeserver does not support the features required to join this room",
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// UnsupportedRoomVersion is an error which is returned when the client
|
|
||||||
// requests a room with a version that is unsupported.
|
|
||||||
func UnsupportedRoomVersion(msg string) *MatrixError {
|
|
||||||
return &MatrixError{"M_UNSUPPORTED_ROOM_VERSION", msg}
|
|
||||||
}
|
|
||||||
|
|
||||||
// LimitExceededError is a rate-limiting error.
|
|
||||||
type LimitExceededError struct {
|
|
||||||
MatrixError
|
|
||||||
RetryAfterMS int64 `json:"retry_after_ms,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// LimitExceeded is an error when the client tries to send events too quickly.
|
|
||||||
func LimitExceeded(msg string, retryAfterMS int64) *LimitExceededError {
|
|
||||||
return &LimitExceededError{
|
|
||||||
MatrixError: MatrixError{"M_LIMIT_EXCEEDED", msg},
|
|
||||||
RetryAfterMS: retryAfterMS,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// NotTrusted is an error which is returned when the client asks the server to
|
|
||||||
// proxy a request (e.g. 3PID association) to a server that isn't trusted
|
|
||||||
func NotTrusted(serverName string) *MatrixError {
|
|
||||||
return &MatrixError{
|
|
||||||
ErrCode: "M_SERVER_NOT_TRUSTED",
|
|
||||||
Err: fmt.Sprintf("Untrusted server '%s'", serverName),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// InternalAPIError is returned when Dendrite failed to reach an internal API.
|
|
||||||
func InternalAPIError(ctx context.Context, err error) util.JSONResponse {
|
|
||||||
logrus.WithContext(ctx).WithError(err).Error("Error reaching an internal API")
|
|
||||||
return util.JSONResponse{
|
|
||||||
Code: http.StatusInternalServerError,
|
|
||||||
JSON: &MatrixError{
|
|
||||||
ErrCode: "M_INTERNAL_SERVER_ERROR",
|
|
||||||
Err: "Dendrite encountered an error reaching an internal API.",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,44 +0,0 @@
|
||||||
// Copyright 2017 Vector Creations Ltd
|
|
||||||
//
|
|
||||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
// you may not use this file except in compliance with the License.
|
|
||||||
// You may obtain a copy of the License at
|
|
||||||
//
|
|
||||||
// http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
//
|
|
||||||
// Unless required by applicable law or agreed to in writing, software
|
|
||||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
// See the License for the specific language governing permissions and
|
|
||||||
// limitations under the License.
|
|
||||||
|
|
||||||
package jsonerror
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/json"
|
|
||||||
"testing"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestLimitExceeded(t *testing.T) {
|
|
||||||
e := LimitExceeded("too fast", 5000)
|
|
||||||
jsonBytes, err := json.Marshal(&e)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("TestLimitExceeded: Failed to marshal LimitExceeded error. %s", err.Error())
|
|
||||||
}
|
|
||||||
want := `{"errcode":"M_LIMIT_EXCEEDED","error":"too fast","retry_after_ms":5000}`
|
|
||||||
if string(jsonBytes) != want {
|
|
||||||
t.Errorf("TestLimitExceeded: want %s, got %s", want, string(jsonBytes))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestForbidden(t *testing.T) {
|
|
||||||
e := Forbidden("you shall not pass")
|
|
||||||
jsonBytes, err := json.Marshal(&e)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("TestForbidden: Failed to marshal Forbidden error. %s", err.Error())
|
|
||||||
}
|
|
||||||
want := `{"errcode":"M_FORBIDDEN","error":"you shall not pass"}`
|
|
||||||
if string(jsonBytes) != want {
|
|
||||||
t.Errorf("TestForbidden: want %s, got %s", want, string(jsonBytes))
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -22,6 +22,7 @@ import (
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/matrix-org/gomatrixserverlib"
|
"github.com/matrix-org/gomatrixserverlib"
|
||||||
|
"github.com/matrix-org/gomatrixserverlib/spec"
|
||||||
"github.com/nats-io/nats.go"
|
"github.com/nats-io/nats.go"
|
||||||
log "github.com/sirupsen/logrus"
|
log "github.com/sirupsen/logrus"
|
||||||
|
|
||||||
|
@ -37,13 +38,13 @@ type SyncAPIProducer struct {
|
||||||
TopicTypingEvent string
|
TopicTypingEvent string
|
||||||
TopicPresenceEvent string
|
TopicPresenceEvent string
|
||||||
JetStream nats.JetStreamContext
|
JetStream nats.JetStreamContext
|
||||||
ServerName gomatrixserverlib.ServerName
|
ServerName spec.ServerName
|
||||||
UserAPI userapi.ClientUserAPI
|
UserAPI userapi.ClientUserAPI
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *SyncAPIProducer) SendReceipt(
|
func (p *SyncAPIProducer) SendReceipt(
|
||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
userID, roomID, eventID, receiptType string, timestamp gomatrixserverlib.Timestamp,
|
userID, roomID, eventID, receiptType string, timestamp spec.Timestamp,
|
||||||
) error {
|
) error {
|
||||||
m := &nats.Msg{
|
m := &nats.Msg{
|
||||||
Subject: p.TopicReceiptEvent,
|
Subject: p.TopicReceiptEvent,
|
||||||
|
@ -154,7 +155,7 @@ func (p *SyncAPIProducer) SendPresence(
|
||||||
m.Header.Set("status_msg", *statusMsg)
|
m.Header.Set("status_msg", *statusMsg)
|
||||||
}
|
}
|
||||||
|
|
||||||
m.Header.Set("last_active_ts", strconv.Itoa(int(gomatrixserverlib.AsTimestamp(time.Now()))))
|
m.Header.Set("last_active_ts", strconv.Itoa(int(spec.AsTimestamp(time.Now()))))
|
||||||
|
|
||||||
_, err := p.JetStream.PublishMsg(m, nats.Context(ctx))
|
_, err := p.JetStream.PublishMsg(m, nats.Context(ctx))
|
||||||
return err
|
return err
|
||||||
|
|
|
@ -21,11 +21,11 @@ import (
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
"github.com/matrix-org/dendrite/clientapi/httputil"
|
"github.com/matrix-org/dendrite/clientapi/httputil"
|
||||||
"github.com/matrix-org/dendrite/clientapi/jsonerror"
|
|
||||||
"github.com/matrix-org/dendrite/clientapi/producers"
|
"github.com/matrix-org/dendrite/clientapi/producers"
|
||||||
"github.com/matrix-org/dendrite/internal/eventutil"
|
"github.com/matrix-org/dendrite/internal/eventutil"
|
||||||
roomserverAPI "github.com/matrix-org/dendrite/roomserver/api"
|
roomserverAPI "github.com/matrix-org/dendrite/roomserver/api"
|
||||||
"github.com/matrix-org/dendrite/userapi/api"
|
"github.com/matrix-org/dendrite/userapi/api"
|
||||||
|
"github.com/matrix-org/gomatrixserverlib/spec"
|
||||||
|
|
||||||
"github.com/matrix-org/util"
|
"github.com/matrix-org/util"
|
||||||
)
|
)
|
||||||
|
@ -38,7 +38,7 @@ func GetAccountData(
|
||||||
if userID != device.UserID {
|
if userID != device.UserID {
|
||||||
return util.JSONResponse{
|
return util.JSONResponse{
|
||||||
Code: http.StatusForbidden,
|
Code: http.StatusForbidden,
|
||||||
JSON: jsonerror.Forbidden("userID does not match the current user"),
|
JSON: spec.Forbidden("userID does not match the current user"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -69,7 +69,7 @@ func GetAccountData(
|
||||||
|
|
||||||
return util.JSONResponse{
|
return util.JSONResponse{
|
||||||
Code: http.StatusNotFound,
|
Code: http.StatusNotFound,
|
||||||
JSON: jsonerror.NotFound("data not found"),
|
JSON: spec.NotFound("data not found"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -81,7 +81,7 @@ func SaveAccountData(
|
||||||
if userID != device.UserID {
|
if userID != device.UserID {
|
||||||
return util.JSONResponse{
|
return util.JSONResponse{
|
||||||
Code: http.StatusForbidden,
|
Code: http.StatusForbidden,
|
||||||
JSON: jsonerror.Forbidden("userID does not match the current user"),
|
JSON: spec.Forbidden("userID does not match the current user"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -90,27 +90,30 @@ func SaveAccountData(
|
||||||
if req.Body == http.NoBody {
|
if req.Body == http.NoBody {
|
||||||
return util.JSONResponse{
|
return util.JSONResponse{
|
||||||
Code: http.StatusBadRequest,
|
Code: http.StatusBadRequest,
|
||||||
JSON: jsonerror.NotJSON("Content not JSON"),
|
JSON: spec.NotJSON("Content not JSON"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if dataType == "m.fully_read" || dataType == "m.push_rules" {
|
if dataType == "m.fully_read" || dataType == "m.push_rules" {
|
||||||
return util.JSONResponse{
|
return util.JSONResponse{
|
||||||
Code: http.StatusForbidden,
|
Code: http.StatusForbidden,
|
||||||
JSON: jsonerror.Forbidden(fmt.Sprintf("Unable to modify %q using this API", dataType)),
|
JSON: spec.Forbidden(fmt.Sprintf("Unable to modify %q using this API", dataType)),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
body, err := io.ReadAll(req.Body)
|
body, err := io.ReadAll(req.Body)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
util.GetLogger(req.Context()).WithError(err).Error("io.ReadAll failed")
|
util.GetLogger(req.Context()).WithError(err).Error("io.ReadAll failed")
|
||||||
return jsonerror.InternalServerError()
|
return util.JSONResponse{
|
||||||
|
Code: http.StatusInternalServerError,
|
||||||
|
JSON: spec.InternalServerError{},
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if !json.Valid(body) {
|
if !json.Valid(body) {
|
||||||
return util.JSONResponse{
|
return util.JSONResponse{
|
||||||
Code: http.StatusBadRequest,
|
Code: http.StatusBadRequest,
|
||||||
JSON: jsonerror.BadJSON("Bad JSON content"),
|
JSON: spec.BadJSON("Bad JSON content"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -142,8 +145,16 @@ func SaveReadMarker(
|
||||||
userAPI api.ClientUserAPI, rsAPI roomserverAPI.ClientRoomserverAPI,
|
userAPI api.ClientUserAPI, rsAPI roomserverAPI.ClientRoomserverAPI,
|
||||||
syncProducer *producers.SyncAPIProducer, device *api.Device, roomID string,
|
syncProducer *producers.SyncAPIProducer, device *api.Device, roomID string,
|
||||||
) util.JSONResponse {
|
) util.JSONResponse {
|
||||||
|
deviceUserID, err := spec.NewUserID(device.UserID, true)
|
||||||
|
if err != nil {
|
||||||
|
return util.JSONResponse{
|
||||||
|
Code: http.StatusBadRequest,
|
||||||
|
JSON: spec.BadJSON("userID for this device is invalid"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Verify that the user is a member of this room
|
// Verify that the user is a member of this room
|
||||||
resErr := checkMemberInRoom(req.Context(), rsAPI, device.UserID, roomID)
|
resErr := checkMemberInRoom(req.Context(), rsAPI, *deviceUserID, roomID)
|
||||||
if resErr != nil {
|
if resErr != nil {
|
||||||
return *resErr
|
return *resErr
|
||||||
}
|
}
|
||||||
|
@ -157,7 +168,10 @@ func SaveReadMarker(
|
||||||
if r.FullyRead != "" {
|
if r.FullyRead != "" {
|
||||||
data, err := json.Marshal(fullyReadEvent{EventID: r.FullyRead})
|
data, err := json.Marshal(fullyReadEvent{EventID: r.FullyRead})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return jsonerror.InternalServerError()
|
return util.JSONResponse{
|
||||||
|
Code: http.StatusInternalServerError,
|
||||||
|
JSON: spec.InternalServerError{},
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
dataReq := api.InputAccountDataRequest{
|
dataReq := api.InputAccountDataRequest{
|
||||||
|
|
|
@ -3,129 +3,328 @@ package routing
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"regexp"
|
||||||
|
"strconv"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/gorilla/mux"
|
"github.com/gorilla/mux"
|
||||||
"github.com/matrix-org/dendrite/internal"
|
"github.com/matrix-org/dendrite/internal"
|
||||||
|
"github.com/matrix-org/dendrite/internal/eventutil"
|
||||||
"github.com/matrix-org/gomatrixserverlib"
|
"github.com/matrix-org/gomatrixserverlib"
|
||||||
|
"github.com/matrix-org/gomatrixserverlib/spec"
|
||||||
"github.com/matrix-org/util"
|
"github.com/matrix-org/util"
|
||||||
"github.com/nats-io/nats.go"
|
"github.com/nats-io/nats.go"
|
||||||
"github.com/sirupsen/logrus"
|
"github.com/sirupsen/logrus"
|
||||||
|
"golang.org/x/exp/constraints"
|
||||||
|
|
||||||
"github.com/matrix-org/dendrite/clientapi/jsonerror"
|
clientapi "github.com/matrix-org/dendrite/clientapi/api"
|
||||||
"github.com/matrix-org/dendrite/internal/httputil"
|
"github.com/matrix-org/dendrite/internal/httputil"
|
||||||
roomserverAPI "github.com/matrix-org/dendrite/roomserver/api"
|
roomserverAPI "github.com/matrix-org/dendrite/roomserver/api"
|
||||||
"github.com/matrix-org/dendrite/setup/config"
|
"github.com/matrix-org/dendrite/setup/config"
|
||||||
"github.com/matrix-org/dendrite/setup/jetstream"
|
"github.com/matrix-org/dendrite/setup/jetstream"
|
||||||
"github.com/matrix-org/dendrite/userapi/api"
|
"github.com/matrix-org/dendrite/userapi/api"
|
||||||
|
userapi "github.com/matrix-org/dendrite/userapi/api"
|
||||||
)
|
)
|
||||||
|
|
||||||
func AdminEvacuateRoom(req *http.Request, cfg *config.ClientAPI, device *api.Device, rsAPI roomserverAPI.ClientRoomserverAPI) util.JSONResponse {
|
var validRegistrationTokenRegex = regexp.MustCompile("^[[:ascii:][:digit:]_]*$")
|
||||||
vars, err := httputil.URLDecodeMapValues(mux.Vars(req))
|
|
||||||
if err != nil {
|
func AdminCreateNewRegistrationToken(req *http.Request, cfg *config.ClientAPI, userAPI userapi.ClientUserAPI) util.JSONResponse {
|
||||||
return util.ErrorResponse(err)
|
if !cfg.RegistrationRequiresToken {
|
||||||
}
|
|
||||||
roomID, ok := vars["roomID"]
|
|
||||||
if !ok {
|
|
||||||
return util.JSONResponse{
|
return util.JSONResponse{
|
||||||
Code: http.StatusBadRequest,
|
Code: http.StatusForbidden,
|
||||||
JSON: jsonerror.MissingArgument("Expecting room ID."),
|
JSON: spec.Forbidden("Registration via tokens is not enabled on this homeserver"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
res := &roomserverAPI.PerformAdminEvacuateRoomResponse{}
|
request := struct {
|
||||||
if err := rsAPI.PerformAdminEvacuateRoom(
|
Token string `json:"token"`
|
||||||
req.Context(),
|
UsesAllowed *int32 `json:"uses_allowed,omitempty"`
|
||||||
&roomserverAPI.PerformAdminEvacuateRoomRequest{
|
ExpiryTime *int64 `json:"expiry_time,omitempty"`
|
||||||
RoomID: roomID,
|
Length int32 `json:"length"`
|
||||||
},
|
}{}
|
||||||
res,
|
|
||||||
); err != nil {
|
if err := json.NewDecoder(req.Body).Decode(&request); err != nil {
|
||||||
return util.ErrorResponse(err)
|
return util.JSONResponse{
|
||||||
|
Code: http.StatusBadRequest,
|
||||||
|
JSON: spec.BadJSON(fmt.Sprintf("Failed to decode request body: %s", err)),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if err := res.Error; err != nil {
|
|
||||||
return err.JSONResponse()
|
token := request.Token
|
||||||
|
usesAllowed := request.UsesAllowed
|
||||||
|
expiryTime := request.ExpiryTime
|
||||||
|
length := request.Length
|
||||||
|
|
||||||
|
if len(token) == 0 {
|
||||||
|
if length == 0 {
|
||||||
|
// length not provided in request. Assign default value of 16.
|
||||||
|
length = 16
|
||||||
|
}
|
||||||
|
// token not present in request body. Hence, generate a random token.
|
||||||
|
if length <= 0 || length > 64 {
|
||||||
|
return util.JSONResponse{
|
||||||
|
Code: http.StatusBadRequest,
|
||||||
|
JSON: spec.BadJSON("length must be greater than zero and not greater than 64"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
token = util.RandomString(int(length))
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(token) > 64 {
|
||||||
|
//Token present in request body, but is too long.
|
||||||
|
return util.JSONResponse{
|
||||||
|
Code: http.StatusBadRequest,
|
||||||
|
JSON: spec.BadJSON("token must not be longer than 64"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
isTokenValid := validRegistrationTokenRegex.Match([]byte(token))
|
||||||
|
if !isTokenValid {
|
||||||
|
return util.JSONResponse{
|
||||||
|
Code: http.StatusBadRequest,
|
||||||
|
JSON: spec.BadJSON("token must consist only of characters matched by the regex [A-Za-z0-9-_]"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// At this point, we have a valid token, either through request body or through random generation.
|
||||||
|
if usesAllowed != nil && *usesAllowed < 0 {
|
||||||
|
return util.JSONResponse{
|
||||||
|
Code: http.StatusBadRequest,
|
||||||
|
JSON: spec.BadJSON("uses_allowed must be a non-negative integer or null"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if expiryTime != nil && spec.Timestamp(*expiryTime).Time().Before(time.Now()) {
|
||||||
|
return util.JSONResponse{
|
||||||
|
Code: http.StatusBadRequest,
|
||||||
|
JSON: spec.BadJSON("expiry_time must not be in the past"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pending := int32(0)
|
||||||
|
completed := int32(0)
|
||||||
|
// If usesAllowed or expiryTime is 0, it means they are not present in the request. NULL (indicating unlimited uses / no expiration will be persisted in DB)
|
||||||
|
registrationToken := &clientapi.RegistrationToken{
|
||||||
|
Token: &token,
|
||||||
|
UsesAllowed: usesAllowed,
|
||||||
|
Pending: &pending,
|
||||||
|
Completed: &completed,
|
||||||
|
ExpiryTime: expiryTime,
|
||||||
|
}
|
||||||
|
created, err := userAPI.PerformAdminCreateRegistrationToken(req.Context(), registrationToken)
|
||||||
|
if !created {
|
||||||
|
return util.JSONResponse{
|
||||||
|
Code: http.StatusConflict,
|
||||||
|
JSON: map[string]string{
|
||||||
|
"error": fmt.Sprintf("token: %s already exists", token),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return util.JSONResponse{
|
||||||
|
Code: http.StatusInternalServerError,
|
||||||
|
JSON: err,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return util.JSONResponse{
|
return util.JSONResponse{
|
||||||
Code: 200,
|
Code: 200,
|
||||||
JSON: map[string]interface{}{
|
JSON: map[string]interface{}{
|
||||||
"affected": res.Affected,
|
"token": token,
|
||||||
|
"uses_allowed": getReturnValue(usesAllowed),
|
||||||
|
"pending": pending,
|
||||||
|
"completed": completed,
|
||||||
|
"expiry_time": getReturnValue(expiryTime),
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func AdminEvacuateUser(req *http.Request, cfg *config.ClientAPI, device *api.Device, rsAPI roomserverAPI.ClientRoomserverAPI) util.JSONResponse {
|
func getReturnValue[t constraints.Integer](in *t) any {
|
||||||
|
if in == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return *in
|
||||||
|
}
|
||||||
|
|
||||||
|
func AdminListRegistrationTokens(req *http.Request, cfg *config.ClientAPI, userAPI userapi.ClientUserAPI) util.JSONResponse {
|
||||||
|
queryParams := req.URL.Query()
|
||||||
|
returnAll := true
|
||||||
|
valid := true
|
||||||
|
validQuery, ok := queryParams["valid"]
|
||||||
|
if ok {
|
||||||
|
returnAll = false
|
||||||
|
validValue, err := strconv.ParseBool(validQuery[0])
|
||||||
|
if err != nil {
|
||||||
|
return util.JSONResponse{
|
||||||
|
Code: http.StatusBadRequest,
|
||||||
|
JSON: spec.BadJSON("invalid 'valid' query parameter"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
valid = validValue
|
||||||
|
}
|
||||||
|
tokens, err := userAPI.PerformAdminListRegistrationTokens(req.Context(), returnAll, valid)
|
||||||
|
if err != nil {
|
||||||
|
return util.JSONResponse{
|
||||||
|
Code: http.StatusInternalServerError,
|
||||||
|
JSON: spec.ErrorUnknown,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return util.JSONResponse{
|
||||||
|
Code: 200,
|
||||||
|
JSON: map[string]interface{}{
|
||||||
|
"registration_tokens": tokens,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func AdminGetRegistrationToken(req *http.Request, cfg *config.ClientAPI, userAPI userapi.ClientUserAPI) util.JSONResponse {
|
||||||
vars, err := httputil.URLDecodeMapValues(mux.Vars(req))
|
vars, err := httputil.URLDecodeMapValues(mux.Vars(req))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return util.ErrorResponse(err)
|
return util.ErrorResponse(err)
|
||||||
}
|
}
|
||||||
userID, ok := vars["userID"]
|
tokenText := vars["token"]
|
||||||
if !ok {
|
token, err := userAPI.PerformAdminGetRegistrationToken(req.Context(), tokenText)
|
||||||
|
if err != nil {
|
||||||
return util.JSONResponse{
|
return util.JSONResponse{
|
||||||
Code: http.StatusBadRequest,
|
Code: http.StatusNotFound,
|
||||||
JSON: jsonerror.MissingArgument("Expecting user ID."),
|
JSON: spec.NotFound(fmt.Sprintf("token: %s not found", tokenText)),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
_, domain, err := gomatrixserverlib.SplitID('@', userID)
|
return util.JSONResponse{
|
||||||
|
Code: 200,
|
||||||
|
JSON: token,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func AdminDeleteRegistrationToken(req *http.Request, cfg *config.ClientAPI, userAPI userapi.ClientUserAPI) util.JSONResponse {
|
||||||
|
vars, err := httputil.URLDecodeMapValues(mux.Vars(req))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
return util.ErrorResponse(err)
|
||||||
|
}
|
||||||
|
tokenText := vars["token"]
|
||||||
|
err = userAPI.PerformAdminDeleteRegistrationToken(req.Context(), tokenText)
|
||||||
|
if err != nil {
|
||||||
|
return util.JSONResponse{
|
||||||
|
Code: http.StatusInternalServerError,
|
||||||
|
JSON: err,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return util.JSONResponse{
|
||||||
|
Code: 200,
|
||||||
|
JSON: map[string]interface{}{},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func AdminUpdateRegistrationToken(req *http.Request, cfg *config.ClientAPI, userAPI userapi.ClientUserAPI) util.JSONResponse {
|
||||||
|
vars, err := httputil.URLDecodeMapValues(mux.Vars(req))
|
||||||
|
if err != nil {
|
||||||
|
return util.ErrorResponse(err)
|
||||||
|
}
|
||||||
|
tokenText := vars["token"]
|
||||||
|
request := make(map[string]*int64)
|
||||||
|
if err = json.NewDecoder(req.Body).Decode(&request); err != nil {
|
||||||
|
return util.JSONResponse{
|
||||||
|
Code: http.StatusBadRequest,
|
||||||
|
JSON: spec.BadJSON(fmt.Sprintf("Failed to decode request body: %s", err)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
newAttributes := make(map[string]interface{})
|
||||||
|
usesAllowed, ok := request["uses_allowed"]
|
||||||
|
if ok {
|
||||||
|
// Only add usesAllowed to newAtrributes if it is present and valid
|
||||||
|
if usesAllowed != nil && *usesAllowed < 0 {
|
||||||
|
return util.JSONResponse{
|
||||||
|
Code: http.StatusBadRequest,
|
||||||
|
JSON: spec.BadJSON("uses_allowed must be a non-negative integer or null"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
newAttributes["usesAllowed"] = usesAllowed
|
||||||
|
}
|
||||||
|
expiryTime, ok := request["expiry_time"]
|
||||||
|
if ok {
|
||||||
|
// Only add expiryTime to newAtrributes if it is present and valid
|
||||||
|
if expiryTime != nil && spec.Timestamp(*expiryTime).Time().Before(time.Now()) {
|
||||||
|
return util.JSONResponse{
|
||||||
|
Code: http.StatusBadRequest,
|
||||||
|
JSON: spec.BadJSON("expiry_time must not be in the past"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
newAttributes["expiryTime"] = expiryTime
|
||||||
|
}
|
||||||
|
if len(newAttributes) == 0 {
|
||||||
|
// No attributes to update. Return existing token
|
||||||
|
return AdminGetRegistrationToken(req, cfg, userAPI)
|
||||||
|
}
|
||||||
|
updatedToken, err := userAPI.PerformAdminUpdateRegistrationToken(req.Context(), tokenText, newAttributes)
|
||||||
|
if err != nil {
|
||||||
|
return util.JSONResponse{
|
||||||
|
Code: http.StatusNotFound,
|
||||||
|
JSON: spec.NotFound(fmt.Sprintf("token: %s not found", tokenText)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return util.JSONResponse{
|
||||||
|
Code: 200,
|
||||||
|
JSON: *updatedToken,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func AdminEvacuateRoom(req *http.Request, rsAPI roomserverAPI.ClientRoomserverAPI) util.JSONResponse {
|
||||||
|
vars, err := httputil.URLDecodeMapValues(mux.Vars(req))
|
||||||
|
if err != nil {
|
||||||
|
return util.ErrorResponse(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
affected, err := rsAPI.PerformAdminEvacuateRoom(req.Context(), vars["roomID"])
|
||||||
|
switch err.(type) {
|
||||||
|
case nil:
|
||||||
|
case eventutil.ErrRoomNoExists:
|
||||||
|
return util.JSONResponse{
|
||||||
|
Code: http.StatusNotFound,
|
||||||
|
JSON: spec.NotFound(err.Error()),
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
logrus.WithError(err).WithField("roomID", vars["roomID"]).Error("Failed to evacuate room")
|
||||||
|
return util.ErrorResponse(err)
|
||||||
|
}
|
||||||
|
return util.JSONResponse{
|
||||||
|
Code: 200,
|
||||||
|
JSON: map[string]interface{}{
|
||||||
|
"affected": affected,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func AdminEvacuateUser(req *http.Request, rsAPI roomserverAPI.ClientRoomserverAPI) util.JSONResponse {
|
||||||
|
vars, err := httputil.URLDecodeMapValues(mux.Vars(req))
|
||||||
|
if err != nil {
|
||||||
|
return util.ErrorResponse(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
affected, err := rsAPI.PerformAdminEvacuateUser(req.Context(), vars["userID"])
|
||||||
|
if err != nil {
|
||||||
|
logrus.WithError(err).WithField("userID", vars["userID"]).Error("Failed to evacuate user")
|
||||||
return util.MessageResponse(http.StatusBadRequest, err.Error())
|
return util.MessageResponse(http.StatusBadRequest, err.Error())
|
||||||
}
|
}
|
||||||
if !cfg.Matrix.IsLocalServerName(domain) {
|
|
||||||
return util.JSONResponse{
|
|
||||||
Code: http.StatusBadRequest,
|
|
||||||
JSON: jsonerror.MissingArgument("User ID must belong to this server."),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
res := &roomserverAPI.PerformAdminEvacuateUserResponse{}
|
|
||||||
if err := rsAPI.PerformAdminEvacuateUser(
|
|
||||||
req.Context(),
|
|
||||||
&roomserverAPI.PerformAdminEvacuateUserRequest{
|
|
||||||
UserID: userID,
|
|
||||||
},
|
|
||||||
res,
|
|
||||||
); err != nil {
|
|
||||||
return jsonerror.InternalAPIError(req.Context(), err)
|
|
||||||
}
|
|
||||||
if err := res.Error; err != nil {
|
|
||||||
return err.JSONResponse()
|
|
||||||
}
|
|
||||||
return util.JSONResponse{
|
return util.JSONResponse{
|
||||||
Code: 200,
|
Code: 200,
|
||||||
JSON: map[string]interface{}{
|
JSON: map[string]interface{}{
|
||||||
"affected": res.Affected,
|
"affected": affected,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func AdminPurgeRoom(req *http.Request, cfg *config.ClientAPI, device *api.Device, rsAPI roomserverAPI.ClientRoomserverAPI) util.JSONResponse {
|
func AdminPurgeRoom(req *http.Request, rsAPI roomserverAPI.ClientRoomserverAPI) util.JSONResponse {
|
||||||
vars, err := httputil.URLDecodeMapValues(mux.Vars(req))
|
vars, err := httputil.URLDecodeMapValues(mux.Vars(req))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return util.ErrorResponse(err)
|
return util.ErrorResponse(err)
|
||||||
}
|
}
|
||||||
roomID, ok := vars["roomID"]
|
|
||||||
if !ok {
|
if err = rsAPI.PerformAdminPurgeRoom(context.Background(), vars["roomID"]); err != nil {
|
||||||
return util.JSONResponse{
|
|
||||||
Code: http.StatusBadRequest,
|
|
||||||
JSON: jsonerror.MissingArgument("Expecting room ID."),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
res := &roomserverAPI.PerformAdminPurgeRoomResponse{}
|
|
||||||
if err := rsAPI.PerformAdminPurgeRoom(
|
|
||||||
context.Background(),
|
|
||||||
&roomserverAPI.PerformAdminPurgeRoomRequest{
|
|
||||||
RoomID: roomID,
|
|
||||||
},
|
|
||||||
res,
|
|
||||||
); err != nil {
|
|
||||||
return util.ErrorResponse(err)
|
return util.ErrorResponse(err)
|
||||||
}
|
}
|
||||||
if err := res.Error; err != nil {
|
|
||||||
return err.JSONResponse()
|
|
||||||
}
|
|
||||||
return util.JSONResponse{
|
return util.JSONResponse{
|
||||||
Code: 200,
|
Code: 200,
|
||||||
JSON: res,
|
JSON: struct{}{},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -133,7 +332,7 @@ func AdminResetPassword(req *http.Request, cfg *config.ClientAPI, device *api.De
|
||||||
if req.Body == nil {
|
if req.Body == nil {
|
||||||
return util.JSONResponse{
|
return util.JSONResponse{
|
||||||
Code: http.StatusBadRequest,
|
Code: http.StatusBadRequest,
|
||||||
JSON: jsonerror.Unknown("Missing request body"),
|
JSON: spec.Unknown("Missing request body"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
vars, err := httputil.URLDecodeMapValues(mux.Vars(req))
|
vars, err := httputil.URLDecodeMapValues(mux.Vars(req))
|
||||||
|
@ -146,7 +345,7 @@ func AdminResetPassword(req *http.Request, cfg *config.ClientAPI, device *api.De
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return util.JSONResponse{
|
return util.JSONResponse{
|
||||||
Code: http.StatusBadRequest,
|
Code: http.StatusBadRequest,
|
||||||
JSON: jsonerror.InvalidArgumentValue(err.Error()),
|
JSON: spec.InvalidParam(err.Error()),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
accAvailableResp := &api.QueryAccountAvailabilityResponse{}
|
accAvailableResp := &api.QueryAccountAvailabilityResponse{}
|
||||||
|
@ -156,28 +355,29 @@ func AdminResetPassword(req *http.Request, cfg *config.ClientAPI, device *api.De
|
||||||
}, accAvailableResp); err != nil {
|
}, accAvailableResp); err != nil {
|
||||||
return util.JSONResponse{
|
return util.JSONResponse{
|
||||||
Code: http.StatusInternalServerError,
|
Code: http.StatusInternalServerError,
|
||||||
JSON: jsonerror.InternalAPIError(req.Context(), err),
|
JSON: spec.InternalServerError{},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if accAvailableResp.Available {
|
if accAvailableResp.Available {
|
||||||
return util.JSONResponse{
|
return util.JSONResponse{
|
||||||
Code: http.StatusNotFound,
|
Code: http.StatusNotFound,
|
||||||
JSON: jsonerror.Unknown("User does not exist"),
|
JSON: spec.Unknown("User does not exist"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
request := struct {
|
request := struct {
|
||||||
Password string `json:"password"`
|
Password string `json:"password"`
|
||||||
|
LogoutDevices bool `json:"logout_devices"`
|
||||||
}{}
|
}{}
|
||||||
if err = json.NewDecoder(req.Body).Decode(&request); err != nil {
|
if err = json.NewDecoder(req.Body).Decode(&request); err != nil {
|
||||||
return util.JSONResponse{
|
return util.JSONResponse{
|
||||||
Code: http.StatusBadRequest,
|
Code: http.StatusBadRequest,
|
||||||
JSON: jsonerror.Unknown("Failed to decode request body: " + err.Error()),
|
JSON: spec.Unknown("Failed to decode request body: " + err.Error()),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if request.Password == "" {
|
if request.Password == "" {
|
||||||
return util.JSONResponse{
|
return util.JSONResponse{
|
||||||
Code: http.StatusBadRequest,
|
Code: http.StatusBadRequest,
|
||||||
JSON: jsonerror.MissingArgument("Expecting non-empty password."),
|
JSON: spec.MissingParam("Expecting non-empty password."),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -189,13 +389,13 @@ func AdminResetPassword(req *http.Request, cfg *config.ClientAPI, device *api.De
|
||||||
Localpart: localpart,
|
Localpart: localpart,
|
||||||
ServerName: serverName,
|
ServerName: serverName,
|
||||||
Password: request.Password,
|
Password: request.Password,
|
||||||
LogoutDevices: true,
|
LogoutDevices: request.LogoutDevices,
|
||||||
}
|
}
|
||||||
updateRes := &api.PerformPasswordUpdateResponse{}
|
updateRes := &api.PerformPasswordUpdateResponse{}
|
||||||
if err := userAPI.PerformPasswordUpdate(req.Context(), updateReq, updateRes); err != nil {
|
if err := userAPI.PerformPasswordUpdate(req.Context(), updateReq, updateRes); err != nil {
|
||||||
return util.JSONResponse{
|
return util.JSONResponse{
|
||||||
Code: http.StatusBadRequest,
|
Code: http.StatusBadRequest,
|
||||||
JSON: jsonerror.Unknown("Failed to perform password update: " + err.Error()),
|
JSON: spec.Unknown("Failed to perform password update: " + err.Error()),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return util.JSONResponse{
|
return util.JSONResponse{
|
||||||
|
@ -212,7 +412,10 @@ func AdminReindex(req *http.Request, cfg *config.ClientAPI, device *api.Device,
|
||||||
_, err := natsClient.RequestMsg(nats.NewMsg(cfg.Matrix.JetStream.Prefixed(jetstream.InputFulltextReindex)), time.Second*10)
|
_, err := natsClient.RequestMsg(nats.NewMsg(cfg.Matrix.JetStream.Prefixed(jetstream.InputFulltextReindex)), time.Second*10)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logrus.WithError(err).Error("failed to publish nats message")
|
logrus.WithError(err).Error("failed to publish nats message")
|
||||||
return jsonerror.InternalServerError()
|
return util.JSONResponse{
|
||||||
|
Code: http.StatusInternalServerError,
|
||||||
|
JSON: spec.InternalServerError{},
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return util.JSONResponse{
|
return util.JSONResponse{
|
||||||
Code: http.StatusOK,
|
Code: http.StatusOK,
|
||||||
|
@ -234,7 +437,7 @@ func AdminMarkAsStale(req *http.Request, cfg *config.ClientAPI, keyAPI api.Clien
|
||||||
if cfg.Matrix.IsLocalServerName(domain) {
|
if cfg.Matrix.IsLocalServerName(domain) {
|
||||||
return util.JSONResponse{
|
return util.JSONResponse{
|
||||||
Code: http.StatusBadRequest,
|
Code: http.StatusBadRequest,
|
||||||
JSON: jsonerror.InvalidParam("Can not mark local device list as stale"),
|
JSON: spec.InvalidParam("Can not mark local device list as stale"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -245,7 +448,7 @@ func AdminMarkAsStale(req *http.Request, cfg *config.ClientAPI, keyAPI api.Clien
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return util.JSONResponse{
|
return util.JSONResponse{
|
||||||
Code: http.StatusInternalServerError,
|
Code: http.StatusInternalServerError,
|
||||||
JSON: jsonerror.Unknown(fmt.Sprintf("Failed to mark device list as stale: %s", err)),
|
JSON: spec.Unknown(fmt.Sprintf("Failed to mark device list as stale: %s", err)),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return util.JSONResponse{
|
return util.JSONResponse{
|
||||||
|
@ -254,7 +457,7 @@ func AdminMarkAsStale(req *http.Request, cfg *config.ClientAPI, keyAPI api.Clien
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func AdminDownloadState(req *http.Request, cfg *config.ClientAPI, device *api.Device, rsAPI roomserverAPI.ClientRoomserverAPI) util.JSONResponse {
|
func AdminDownloadState(req *http.Request, device *api.Device, rsAPI roomserverAPI.ClientRoomserverAPI) util.JSONResponse {
|
||||||
vars, err := httputil.URLDecodeMapValues(mux.Vars(req))
|
vars, err := httputil.URLDecodeMapValues(mux.Vars(req))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return util.ErrorResponse(err)
|
return util.ErrorResponse(err)
|
||||||
|
@ -263,33 +466,122 @@ func AdminDownloadState(req *http.Request, cfg *config.ClientAPI, device *api.De
|
||||||
if !ok {
|
if !ok {
|
||||||
return util.JSONResponse{
|
return util.JSONResponse{
|
||||||
Code: http.StatusBadRequest,
|
Code: http.StatusBadRequest,
|
||||||
JSON: jsonerror.MissingArgument("Expecting room ID."),
|
JSON: spec.MissingParam("Expecting room ID."),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
serverName, ok := vars["serverName"]
|
serverName, ok := vars["serverName"]
|
||||||
if !ok {
|
if !ok {
|
||||||
return util.JSONResponse{
|
return util.JSONResponse{
|
||||||
Code: http.StatusBadRequest,
|
Code: http.StatusBadRequest,
|
||||||
JSON: jsonerror.MissingArgument("Expecting remote server name."),
|
JSON: spec.MissingParam("Expecting remote server name."),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
res := &roomserverAPI.PerformAdminDownloadStateResponse{}
|
if err = rsAPI.PerformAdminDownloadState(req.Context(), roomID, device.UserID, spec.ServerName(serverName)); err != nil {
|
||||||
if err := rsAPI.PerformAdminDownloadState(
|
if errors.Is(err, eventutil.ErrRoomNoExists{}) {
|
||||||
req.Context(),
|
return util.JSONResponse{
|
||||||
&roomserverAPI.PerformAdminDownloadStateRequest{
|
Code: 200,
|
||||||
UserID: device.UserID,
|
JSON: spec.NotFound(err.Error()),
|
||||||
RoomID: roomID,
|
}
|
||||||
ServerName: gomatrixserverlib.ServerName(serverName),
|
}
|
||||||
},
|
logrus.WithError(err).WithFields(logrus.Fields{
|
||||||
res,
|
"userID": device.UserID,
|
||||||
); err != nil {
|
"serverName": serverName,
|
||||||
return jsonerror.InternalAPIError(req.Context(), err)
|
"roomID": roomID,
|
||||||
}
|
}).Error("failed to download state")
|
||||||
if err := res.Error; err != nil {
|
return util.ErrorResponse(err)
|
||||||
return err.JSONResponse()
|
|
||||||
}
|
}
|
||||||
return util.JSONResponse{
|
return util.JSONResponse{
|
||||||
Code: 200,
|
Code: 200,
|
||||||
JSON: map[string]interface{}{},
|
JSON: struct{}{},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetEventReports returns reported events for a given user/room.
|
||||||
|
func GetEventReports(
|
||||||
|
req *http.Request,
|
||||||
|
rsAPI roomserverAPI.ClientRoomserverAPI,
|
||||||
|
from, limit uint64,
|
||||||
|
backwards bool,
|
||||||
|
userID, roomID string,
|
||||||
|
) util.JSONResponse {
|
||||||
|
|
||||||
|
eventReports, count, err := rsAPI.QueryAdminEventReports(req.Context(), from, limit, backwards, userID, roomID)
|
||||||
|
if err != nil {
|
||||||
|
logrus.WithError(err).Error("failed to query event reports")
|
||||||
|
return util.JSONResponse{
|
||||||
|
Code: http.StatusInternalServerError,
|
||||||
|
JSON: spec.InternalServerError{},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
resp := map[string]any{
|
||||||
|
"event_reports": eventReports,
|
||||||
|
"total": count,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add a next_token if there are still reports
|
||||||
|
if int64(from+limit) < count {
|
||||||
|
resp["next_token"] = int(from) + len(eventReports)
|
||||||
|
}
|
||||||
|
|
||||||
|
return util.JSONResponse{
|
||||||
|
Code: http.StatusOK,
|
||||||
|
JSON: resp,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetEventReport(req *http.Request, rsAPI roomserverAPI.ClientRoomserverAPI, reportID string) util.JSONResponse {
|
||||||
|
parsedReportID, err := strconv.ParseUint(reportID, 10, 64)
|
||||||
|
if err != nil {
|
||||||
|
return util.JSONResponse{
|
||||||
|
Code: http.StatusBadRequest,
|
||||||
|
// Given this is an admin endpoint, let them know what didn't work.
|
||||||
|
JSON: spec.InvalidParam(err.Error()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
report, err := rsAPI.QueryAdminEventReport(req.Context(), parsedReportID)
|
||||||
|
if err != nil {
|
||||||
|
return util.JSONResponse{
|
||||||
|
Code: http.StatusInternalServerError,
|
||||||
|
JSON: spec.Unknown(err.Error()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return util.JSONResponse{
|
||||||
|
Code: http.StatusOK,
|
||||||
|
JSON: report,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func DeleteEventReport(req *http.Request, rsAPI roomserverAPI.ClientRoomserverAPI, reportID string) util.JSONResponse {
|
||||||
|
parsedReportID, err := strconv.ParseUint(reportID, 10, 64)
|
||||||
|
if err != nil {
|
||||||
|
return util.JSONResponse{
|
||||||
|
Code: http.StatusBadRequest,
|
||||||
|
// Given this is an admin endpoint, let them know what didn't work.
|
||||||
|
JSON: spec.InvalidParam(err.Error()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
err = rsAPI.PerformAdminDeleteEventReport(req.Context(), parsedReportID)
|
||||||
|
if err != nil {
|
||||||
|
return util.JSONResponse{
|
||||||
|
Code: http.StatusInternalServerError,
|
||||||
|
JSON: spec.Unknown(err.Error()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return util.JSONResponse{
|
||||||
|
Code: http.StatusOK,
|
||||||
|
JSON: struct{}{},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseUint64OrDefault(input string, defaultValue uint64) uint64 {
|
||||||
|
v, err := strconv.ParseUint(input, 10, 64)
|
||||||
|
if err != nil {
|
||||||
|
return defaultValue
|
||||||
|
}
|
||||||
|
return v
|
||||||
|
}
|
||||||
|
|
|
@ -17,8 +17,8 @@ package routing
|
||||||
import (
|
import (
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
"github.com/matrix-org/dendrite/clientapi/jsonerror"
|
|
||||||
"github.com/matrix-org/dendrite/userapi/api"
|
"github.com/matrix-org/dendrite/userapi/api"
|
||||||
|
"github.com/matrix-org/gomatrixserverlib/spec"
|
||||||
|
|
||||||
"github.com/matrix-org/util"
|
"github.com/matrix-org/util"
|
||||||
)
|
)
|
||||||
|
@ -51,7 +51,7 @@ func GetAdminWhois(
|
||||||
if !allowed {
|
if !allowed {
|
||||||
return util.JSONResponse{
|
return util.JSONResponse{
|
||||||
Code: http.StatusForbidden,
|
Code: http.StatusForbidden,
|
||||||
JSON: jsonerror.Forbidden("userID does not match the current user"),
|
JSON: spec.Forbidden("userID does not match the current user"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -61,7 +61,10 @@ func GetAdminWhois(
|
||||||
}, &queryRes)
|
}, &queryRes)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
util.GetLogger(req.Context()).WithError(err).Error("GetAdminWhois failed to query user devices")
|
util.GetLogger(req.Context()).WithError(err).Error("GetAdminWhois failed to query user devices")
|
||||||
return jsonerror.InternalServerError()
|
return util.JSONResponse{
|
||||||
|
Code: http.StatusInternalServerError,
|
||||||
|
JSON: spec.InternalServerError{},
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
devices := make(map[string]deviceInfo)
|
devices := make(map[string]deviceInfo)
|
||||||
|
|
|
@ -15,14 +15,14 @@
|
||||||
package routing
|
package routing
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
"github.com/matrix-org/dendrite/clientapi/jsonerror"
|
|
||||||
"github.com/matrix-org/dendrite/roomserver/api"
|
"github.com/matrix-org/dendrite/roomserver/api"
|
||||||
userapi "github.com/matrix-org/dendrite/userapi/api"
|
userapi "github.com/matrix-org/dendrite/userapi/api"
|
||||||
"github.com/matrix-org/gomatrixserverlib"
|
"github.com/matrix-org/gomatrixserverlib"
|
||||||
|
"github.com/matrix-org/gomatrixserverlib/spec"
|
||||||
"github.com/matrix-org/util"
|
"github.com/matrix-org/util"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -31,7 +31,7 @@ func GetAliases(
|
||||||
req *http.Request, rsAPI api.ClientRoomserverAPI, device *userapi.Device, roomID string,
|
req *http.Request, rsAPI api.ClientRoomserverAPI, device *userapi.Device, roomID string,
|
||||||
) util.JSONResponse {
|
) util.JSONResponse {
|
||||||
stateTuple := gomatrixserverlib.StateKeyTuple{
|
stateTuple := gomatrixserverlib.StateKeyTuple{
|
||||||
EventType: gomatrixserverlib.MRoomHistoryVisibility,
|
EventType: spec.MRoomHistoryVisibility,
|
||||||
StateKey: "",
|
StateKey: "",
|
||||||
}
|
}
|
||||||
stateReq := &api.QueryCurrentStateRequest{
|
stateReq := &api.QueryCurrentStateRequest{
|
||||||
|
@ -47,26 +47,37 @@ func GetAliases(
|
||||||
visibility := gomatrixserverlib.HistoryVisibilityInvited
|
visibility := gomatrixserverlib.HistoryVisibilityInvited
|
||||||
if historyVisEvent, ok := stateRes.StateEvents[stateTuple]; ok {
|
if historyVisEvent, ok := stateRes.StateEvents[stateTuple]; ok {
|
||||||
var err error
|
var err error
|
||||||
visibility, err = historyVisEvent.HistoryVisibility()
|
var content gomatrixserverlib.HistoryVisibilityContent
|
||||||
if err != nil {
|
if err = json.Unmarshal(historyVisEvent.Content(), &content); err != nil {
|
||||||
util.GetLogger(req.Context()).WithError(err).Error("historyVisEvent.HistoryVisibility failed")
|
util.GetLogger(req.Context()).WithError(err).Error("historyVisEvent.HistoryVisibility failed")
|
||||||
return util.ErrorResponse(fmt.Errorf("historyVisEvent.HistoryVisibility: %w", err))
|
return util.ErrorResponse(fmt.Errorf("historyVisEvent.HistoryVisibility: %w", err))
|
||||||
}
|
}
|
||||||
|
visibility = content.HistoryVisibility
|
||||||
}
|
}
|
||||||
if visibility != gomatrixserverlib.WorldReadable {
|
if visibility != spec.WorldReadable {
|
||||||
|
deviceUserID, err := spec.NewUserID(device.UserID, true)
|
||||||
|
if err != nil {
|
||||||
|
return util.JSONResponse{
|
||||||
|
Code: http.StatusForbidden,
|
||||||
|
JSON: spec.Forbidden("userID doesn't have power level to change visibility"),
|
||||||
|
}
|
||||||
|
}
|
||||||
queryReq := api.QueryMembershipForUserRequest{
|
queryReq := api.QueryMembershipForUserRequest{
|
||||||
RoomID: roomID,
|
RoomID: roomID,
|
||||||
UserID: device.UserID,
|
UserID: *deviceUserID,
|
||||||
}
|
}
|
||||||
var queryRes api.QueryMembershipForUserResponse
|
var queryRes api.QueryMembershipForUserResponse
|
||||||
if err := rsAPI.QueryMembershipForUser(req.Context(), &queryReq, &queryRes); err != nil {
|
if err := rsAPI.QueryMembershipForUser(req.Context(), &queryReq, &queryRes); err != nil {
|
||||||
util.GetLogger(req.Context()).WithError(err).Error("rsAPI.QueryMembershipsForRoom failed")
|
util.GetLogger(req.Context()).WithError(err).Error("rsAPI.QueryMembershipsForRoom failed")
|
||||||
return jsonerror.InternalServerError()
|
return util.JSONResponse{
|
||||||
|
Code: http.StatusInternalServerError,
|
||||||
|
JSON: spec.InternalServerError{},
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if !queryRes.IsInRoom {
|
if !queryRes.IsInRoom {
|
||||||
return util.JSONResponse{
|
return util.JSONResponse{
|
||||||
Code: http.StatusForbidden,
|
Code: http.StatusForbidden,
|
||||||
JSON: jsonerror.Forbidden("You aren't a member of this room."),
|
JSON: spec.Forbidden("You aren't a member of this room."),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -10,30 +10,28 @@ import (
|
||||||
|
|
||||||
"github.com/matrix-org/dendrite/clientapi/auth/authtypes"
|
"github.com/matrix-org/dendrite/clientapi/auth/authtypes"
|
||||||
"github.com/matrix-org/dendrite/setup/config"
|
"github.com/matrix-org/dendrite/setup/config"
|
||||||
"github.com/matrix-org/dendrite/test/testrig"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func Test_AuthFallback(t *testing.T) {
|
func Test_AuthFallback(t *testing.T) {
|
||||||
base, _, _ := testrig.Base(nil)
|
cfg := config.Dendrite{}
|
||||||
defer base.Close()
|
cfg.Defaults(config.DefaultOpts{Generate: true, SingleDatabase: true})
|
||||||
|
|
||||||
for _, useHCaptcha := range []bool{false, true} {
|
for _, useHCaptcha := range []bool{false, true} {
|
||||||
for _, recaptchaEnabled := range []bool{false, true} {
|
for _, recaptchaEnabled := range []bool{false, true} {
|
||||||
for _, wantErr := range []bool{false, true} {
|
for _, wantErr := range []bool{false, true} {
|
||||||
t.Run(fmt.Sprintf("useHCaptcha(%v) - recaptchaEnabled(%v) - wantErr(%v)", useHCaptcha, recaptchaEnabled, wantErr), func(t *testing.T) {
|
t.Run(fmt.Sprintf("useHCaptcha(%v) - recaptchaEnabled(%v) - wantErr(%v)", useHCaptcha, recaptchaEnabled, wantErr), func(t *testing.T) {
|
||||||
// Set the defaults for each test
|
// Set the defaults for each test
|
||||||
base.Cfg.ClientAPI.Defaults(config.DefaultOpts{Generate: true, SingleDatabase: true})
|
cfg.ClientAPI.Defaults(config.DefaultOpts{Generate: true, SingleDatabase: true})
|
||||||
base.Cfg.ClientAPI.RecaptchaEnabled = recaptchaEnabled
|
cfg.ClientAPI.RecaptchaEnabled = recaptchaEnabled
|
||||||
base.Cfg.ClientAPI.RecaptchaPublicKey = "pub"
|
cfg.ClientAPI.RecaptchaPublicKey = "pub"
|
||||||
base.Cfg.ClientAPI.RecaptchaPrivateKey = "priv"
|
cfg.ClientAPI.RecaptchaPrivateKey = "priv"
|
||||||
if useHCaptcha {
|
if useHCaptcha {
|
||||||
base.Cfg.ClientAPI.RecaptchaSiteVerifyAPI = "https://hcaptcha.com/siteverify"
|
cfg.ClientAPI.RecaptchaSiteVerifyAPI = "https://hcaptcha.com/siteverify"
|
||||||
base.Cfg.ClientAPI.RecaptchaApiJsUrl = "https://js.hcaptcha.com/1/api.js"
|
cfg.ClientAPI.RecaptchaApiJsUrl = "https://js.hcaptcha.com/1/api.js"
|
||||||
base.Cfg.ClientAPI.RecaptchaFormField = "h-captcha-response"
|
cfg.ClientAPI.RecaptchaFormField = "h-captcha-response"
|
||||||
base.Cfg.ClientAPI.RecaptchaSitekeyClass = "h-captcha"
|
cfg.ClientAPI.RecaptchaSitekeyClass = "h-captcha"
|
||||||
}
|
}
|
||||||
cfgErrs := &config.ConfigErrors{}
|
cfgErrs := &config.ConfigErrors{}
|
||||||
base.Cfg.ClientAPI.Verify(cfgErrs)
|
cfg.ClientAPI.Verify(cfgErrs)
|
||||||
if len(*cfgErrs) > 0 {
|
if len(*cfgErrs) > 0 {
|
||||||
t.Fatalf("(hCaptcha=%v) unexpected config errors: %s", useHCaptcha, cfgErrs.Error())
|
t.Fatalf("(hCaptcha=%v) unexpected config errors: %s", useHCaptcha, cfgErrs.Error())
|
||||||
}
|
}
|
||||||
|
@ -41,7 +39,7 @@ func Test_AuthFallback(t *testing.T) {
|
||||||
req := httptest.NewRequest(http.MethodGet, "/?session=1337", nil)
|
req := httptest.NewRequest(http.MethodGet, "/?session=1337", nil)
|
||||||
rec := httptest.NewRecorder()
|
rec := httptest.NewRecorder()
|
||||||
|
|
||||||
AuthFallback(rec, req, authtypes.LoginTypeRecaptcha, &base.Cfg.ClientAPI)
|
AuthFallback(rec, req, authtypes.LoginTypeRecaptcha, &cfg.ClientAPI)
|
||||||
if !recaptchaEnabled {
|
if !recaptchaEnabled {
|
||||||
if rec.Code != http.StatusBadRequest {
|
if rec.Code != http.StatusBadRequest {
|
||||||
t.Fatalf("unexpected response code: %d, want %d", rec.Code, http.StatusBadRequest)
|
t.Fatalf("unexpected response code: %d, want %d", rec.Code, http.StatusBadRequest)
|
||||||
|
@ -50,8 +48,8 @@ func Test_AuthFallback(t *testing.T) {
|
||||||
t.Fatalf("unexpected response body: %s", rec.Body.String())
|
t.Fatalf("unexpected response body: %s", rec.Body.String())
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if !strings.Contains(rec.Body.String(), base.Cfg.ClientAPI.RecaptchaSitekeyClass) {
|
if !strings.Contains(rec.Body.String(), cfg.ClientAPI.RecaptchaSitekeyClass) {
|
||||||
t.Fatalf("body does not contain %s: %s", base.Cfg.ClientAPI.RecaptchaSitekeyClass, rec.Body.String())
|
t.Fatalf("body does not contain %s: %s", cfg.ClientAPI.RecaptchaSitekeyClass, rec.Body.String())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -64,14 +62,14 @@ func Test_AuthFallback(t *testing.T) {
|
||||||
}))
|
}))
|
||||||
defer srv.Close() // nolint: errcheck
|
defer srv.Close() // nolint: errcheck
|
||||||
|
|
||||||
base.Cfg.ClientAPI.RecaptchaSiteVerifyAPI = srv.URL
|
cfg.ClientAPI.RecaptchaSiteVerifyAPI = srv.URL
|
||||||
|
|
||||||
// check the result after sending the captcha
|
// check the result after sending the captcha
|
||||||
req = httptest.NewRequest(http.MethodPost, "/?session=1337", nil)
|
req = httptest.NewRequest(http.MethodPost, "/?session=1337", nil)
|
||||||
req.Form = url.Values{}
|
req.Form = url.Values{}
|
||||||
req.Form.Add(base.Cfg.ClientAPI.RecaptchaFormField, "someRandomValue")
|
req.Form.Add(cfg.ClientAPI.RecaptchaFormField, "someRandomValue")
|
||||||
rec = httptest.NewRecorder()
|
rec = httptest.NewRecorder()
|
||||||
AuthFallback(rec, req, authtypes.LoginTypeRecaptcha, &base.Cfg.ClientAPI)
|
AuthFallback(rec, req, authtypes.LoginTypeRecaptcha, &cfg.ClientAPI)
|
||||||
if recaptchaEnabled {
|
if recaptchaEnabled {
|
||||||
if !wantErr {
|
if !wantErr {
|
||||||
if rec.Code != http.StatusOK {
|
if rec.Code != http.StatusOK {
|
||||||
|
@ -105,7 +103,7 @@ func Test_AuthFallback(t *testing.T) {
|
||||||
t.Run("unknown fallbacks are handled correctly", func(t *testing.T) {
|
t.Run("unknown fallbacks are handled correctly", func(t *testing.T) {
|
||||||
req := httptest.NewRequest(http.MethodPost, "/?session=1337", nil)
|
req := httptest.NewRequest(http.MethodPost, "/?session=1337", nil)
|
||||||
rec := httptest.NewRecorder()
|
rec := httptest.NewRecorder()
|
||||||
AuthFallback(rec, req, "DoesNotExist", &base.Cfg.ClientAPI)
|
AuthFallback(rec, req, "DoesNotExist", &cfg.ClientAPI)
|
||||||
if rec.Code != http.StatusNotImplemented {
|
if rec.Code != http.StatusNotImplemented {
|
||||||
t.Fatalf("unexpected http status: %d, want %d", rec.Code, http.StatusNotImplemented)
|
t.Fatalf("unexpected http status: %d, want %d", rec.Code, http.StatusNotImplemented)
|
||||||
}
|
}
|
||||||
|
@ -114,7 +112,7 @@ func Test_AuthFallback(t *testing.T) {
|
||||||
t.Run("unknown methods are handled correctly", func(t *testing.T) {
|
t.Run("unknown methods are handled correctly", func(t *testing.T) {
|
||||||
req := httptest.NewRequest(http.MethodDelete, "/?session=1337", nil)
|
req := httptest.NewRequest(http.MethodDelete, "/?session=1337", nil)
|
||||||
rec := httptest.NewRecorder()
|
rec := httptest.NewRecorder()
|
||||||
AuthFallback(rec, req, authtypes.LoginTypeRecaptcha, &base.Cfg.ClientAPI)
|
AuthFallback(rec, req, authtypes.LoginTypeRecaptcha, &cfg.ClientAPI)
|
||||||
if rec.Code != http.StatusMethodNotAllowed {
|
if rec.Code != http.StatusMethodNotAllowed {
|
||||||
t.Fatalf("unexpected http status: %d, want %d", rec.Code, http.StatusMethodNotAllowed)
|
t.Fatalf("unexpected http status: %d, want %d", rec.Code, http.StatusMethodNotAllowed)
|
||||||
}
|
}
|
||||||
|
@ -123,7 +121,7 @@ func Test_AuthFallback(t *testing.T) {
|
||||||
t.Run("missing session parameter is handled correctly", func(t *testing.T) {
|
t.Run("missing session parameter is handled correctly", func(t *testing.T) {
|
||||||
req := httptest.NewRequest(http.MethodGet, "/", nil)
|
req := httptest.NewRequest(http.MethodGet, "/", nil)
|
||||||
rec := httptest.NewRecorder()
|
rec := httptest.NewRecorder()
|
||||||
AuthFallback(rec, req, authtypes.LoginTypeRecaptcha, &base.Cfg.ClientAPI)
|
AuthFallback(rec, req, authtypes.LoginTypeRecaptcha, &cfg.ClientAPI)
|
||||||
if rec.Code != http.StatusBadRequest {
|
if rec.Code != http.StatusBadRequest {
|
||||||
t.Fatalf("unexpected http status: %d, want %d", rec.Code, http.StatusBadRequest)
|
t.Fatalf("unexpected http status: %d, want %d", rec.Code, http.StatusBadRequest)
|
||||||
}
|
}
|
||||||
|
@ -132,7 +130,7 @@ func Test_AuthFallback(t *testing.T) {
|
||||||
t.Run("missing session parameter is handled correctly", func(t *testing.T) {
|
t.Run("missing session parameter is handled correctly", func(t *testing.T) {
|
||||||
req := httptest.NewRequest(http.MethodGet, "/", nil)
|
req := httptest.NewRequest(http.MethodGet, "/", nil)
|
||||||
rec := httptest.NewRecorder()
|
rec := httptest.NewRecorder()
|
||||||
AuthFallback(rec, req, authtypes.LoginTypeRecaptcha, &base.Cfg.ClientAPI)
|
AuthFallback(rec, req, authtypes.LoginTypeRecaptcha, &cfg.ClientAPI)
|
||||||
if rec.Code != http.StatusBadRequest {
|
if rec.Code != http.StatusBadRequest {
|
||||||
t.Fatalf("unexpected http status: %d, want %d", rec.Code, http.StatusBadRequest)
|
t.Fatalf("unexpected http status: %d, want %d", rec.Code, http.StatusBadRequest)
|
||||||
}
|
}
|
||||||
|
@ -141,7 +139,7 @@ func Test_AuthFallback(t *testing.T) {
|
||||||
t.Run("missing 'response' is handled correctly", func(t *testing.T) {
|
t.Run("missing 'response' is handled correctly", func(t *testing.T) {
|
||||||
req := httptest.NewRequest(http.MethodPost, "/?session=1337", nil)
|
req := httptest.NewRequest(http.MethodPost, "/?session=1337", nil)
|
||||||
rec := httptest.NewRecorder()
|
rec := httptest.NewRecorder()
|
||||||
AuthFallback(rec, req, authtypes.LoginTypeRecaptcha, &base.Cfg.ClientAPI)
|
AuthFallback(rec, req, authtypes.LoginTypeRecaptcha, &cfg.ClientAPI)
|
||||||
if rec.Code != http.StatusBadRequest {
|
if rec.Code != http.StatusBadRequest {
|
||||||
t.Fatalf("unexpected http status: %d, want %d", rec.Code, http.StatusBadRequest)
|
t.Fatalf("unexpected http status: %d, want %d", rec.Code, http.StatusBadRequest)
|
||||||
}
|
}
|
||||||
|
|
|
@ -17,26 +17,22 @@ package routing
|
||||||
import (
|
import (
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
"github.com/matrix-org/dendrite/clientapi/jsonerror"
|
|
||||||
roomserverAPI "github.com/matrix-org/dendrite/roomserver/api"
|
roomserverAPI "github.com/matrix-org/dendrite/roomserver/api"
|
||||||
|
"github.com/matrix-org/dendrite/roomserver/version"
|
||||||
|
"github.com/matrix-org/gomatrixserverlib"
|
||||||
"github.com/matrix-org/util"
|
"github.com/matrix-org/util"
|
||||||
)
|
)
|
||||||
|
|
||||||
// GetCapabilities returns information about the server's supported feature set
|
// GetCapabilities returns information about the server's supported feature set
|
||||||
// and other relevant capabilities to an authenticated user.
|
// and other relevant capabilities to an authenticated user.
|
||||||
func GetCapabilities(
|
func GetCapabilities(rsAPI roomserverAPI.ClientRoomserverAPI) util.JSONResponse {
|
||||||
req *http.Request, rsAPI roomserverAPI.ClientRoomserverAPI,
|
versionsMap := map[gomatrixserverlib.RoomVersion]string{}
|
||||||
) util.JSONResponse {
|
for v, desc := range version.SupportedRoomVersions() {
|
||||||
roomVersionsQueryReq := roomserverAPI.QueryRoomVersionCapabilitiesRequest{}
|
if desc.Stable() {
|
||||||
roomVersionsQueryRes := roomserverAPI.QueryRoomVersionCapabilitiesResponse{}
|
versionsMap[v] = "stable"
|
||||||
if err := rsAPI.QueryRoomVersionCapabilities(
|
} else {
|
||||||
req.Context(),
|
versionsMap[v] = "unstable"
|
||||||
&roomVersionsQueryReq,
|
}
|
||||||
&roomVersionsQueryRes,
|
|
||||||
); err != nil {
|
|
||||||
util.GetLogger(req.Context()).WithError(err).Error("queryAPI.QueryRoomVersionCapabilities failed")
|
|
||||||
return jsonerror.InternalServerError()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
response := map[string]interface{}{
|
response := map[string]interface{}{
|
||||||
|
@ -44,7 +40,10 @@ func GetCapabilities(
|
||||||
"m.change_password": map[string]bool{
|
"m.change_password": map[string]bool{
|
||||||
"enabled": true,
|
"enabled": true,
|
||||||
},
|
},
|
||||||
"m.room_versions": roomVersionsQueryRes,
|
"m.room_versions": map[string]interface{}{
|
||||||
|
"default": rsAPI.DefaultRoomVersion(),
|
||||||
|
"available": versionsMap,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -26,10 +26,9 @@ import (
|
||||||
roomserverAPI "github.com/matrix-org/dendrite/roomserver/api"
|
roomserverAPI "github.com/matrix-org/dendrite/roomserver/api"
|
||||||
roomserverVersion "github.com/matrix-org/dendrite/roomserver/version"
|
roomserverVersion "github.com/matrix-org/dendrite/roomserver/version"
|
||||||
"github.com/matrix-org/dendrite/userapi/api"
|
"github.com/matrix-org/dendrite/userapi/api"
|
||||||
|
"github.com/matrix-org/gomatrixserverlib/spec"
|
||||||
|
|
||||||
"github.com/matrix-org/dendrite/clientapi/httputil"
|
"github.com/matrix-org/dendrite/clientapi/httputil"
|
||||||
"github.com/matrix-org/dendrite/clientapi/jsonerror"
|
|
||||||
"github.com/matrix-org/dendrite/internal/eventutil"
|
|
||||||
"github.com/matrix-org/dendrite/setup/config"
|
"github.com/matrix-org/dendrite/setup/config"
|
||||||
"github.com/matrix-org/gomatrixserverlib"
|
"github.com/matrix-org/gomatrixserverlib"
|
||||||
"github.com/matrix-org/util"
|
"github.com/matrix-org/util"
|
||||||
|
@ -38,33 +37,19 @@ import (
|
||||||
|
|
||||||
// https://matrix.org/docs/spec/client_server/r0.2.0.html#post-matrix-client-r0-createroom
|
// https://matrix.org/docs/spec/client_server/r0.2.0.html#post-matrix-client-r0-createroom
|
||||||
type createRoomRequest struct {
|
type createRoomRequest struct {
|
||||||
Invite []string `json:"invite"`
|
Invite []string `json:"invite"`
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
Visibility string `json:"visibility"`
|
Visibility string `json:"visibility"`
|
||||||
Topic string `json:"topic"`
|
Topic string `json:"topic"`
|
||||||
Preset string `json:"preset"`
|
Preset string `json:"preset"`
|
||||||
CreationContent json.RawMessage `json:"creation_content"`
|
CreationContent json.RawMessage `json:"creation_content"`
|
||||||
InitialState []fledglingEvent `json:"initial_state"`
|
InitialState []gomatrixserverlib.FledglingEvent `json:"initial_state"`
|
||||||
RoomAliasName string `json:"room_alias_name"`
|
RoomAliasName string `json:"room_alias_name"`
|
||||||
GuestCanJoin bool `json:"guest_can_join"`
|
RoomVersion gomatrixserverlib.RoomVersion `json:"room_version"`
|
||||||
RoomVersion gomatrixserverlib.RoomVersion `json:"room_version"`
|
PowerLevelContentOverride json.RawMessage `json:"power_level_content_override"`
|
||||||
PowerLevelContentOverride json.RawMessage `json:"power_level_content_override"`
|
IsDirect bool `json:"is_direct"`
|
||||||
IsDirect bool `json:"is_direct"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const (
|
|
||||||
presetPrivateChat = "private_chat"
|
|
||||||
presetTrustedPrivateChat = "trusted_private_chat"
|
|
||||||
presetPublicChat = "public_chat"
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
historyVisibilityShared = "shared"
|
|
||||||
// TODO: These should be implemented once history visibility is implemented
|
|
||||||
// historyVisibilityWorldReadable = "world_readable"
|
|
||||||
// historyVisibilityInvited = "invited"
|
|
||||||
)
|
|
||||||
|
|
||||||
func (r createRoomRequest) Validate() *util.JSONResponse {
|
func (r createRoomRequest) Validate() *util.JSONResponse {
|
||||||
whitespace := "\t\n\x0b\x0c\r " // https://docs.python.org/2/library/string.html#string.whitespace
|
whitespace := "\t\n\x0b\x0c\r " // https://docs.python.org/2/library/string.html#string.whitespace
|
||||||
// https://github.com/matrix-org/synapse/blob/v0.19.2/synapse/handlers/room.py#L81
|
// https://github.com/matrix-org/synapse/blob/v0.19.2/synapse/handlers/room.py#L81
|
||||||
|
@ -72,28 +57,23 @@ func (r createRoomRequest) Validate() *util.JSONResponse {
|
||||||
if strings.ContainsAny(r.RoomAliasName, whitespace+":") {
|
if strings.ContainsAny(r.RoomAliasName, whitespace+":") {
|
||||||
return &util.JSONResponse{
|
return &util.JSONResponse{
|
||||||
Code: http.StatusBadRequest,
|
Code: http.StatusBadRequest,
|
||||||
JSON: jsonerror.BadJSON("room_alias_name cannot contain whitespace or ':'"),
|
JSON: spec.BadJSON("room_alias_name cannot contain whitespace or ':'"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
for _, userID := range r.Invite {
|
for _, userID := range r.Invite {
|
||||||
// TODO: We should put user ID parsing code into gomatrixserverlib and use that instead
|
if _, err := spec.NewUserID(userID, true); err != nil {
|
||||||
// (see https://github.com/matrix-org/gomatrixserverlib/blob/3394e7c7003312043208aa73727d2256eea3d1f6/eventcontent.go#L347 )
|
|
||||||
// It should be a struct (with pointers into a single string to avoid copying) and
|
|
||||||
// we should update all refs to use UserID types rather than strings.
|
|
||||||
// https://github.com/matrix-org/synapse/blob/v0.19.2/synapse/types.py#L92
|
|
||||||
if _, _, err := gomatrixserverlib.SplitID('@', userID); err != nil {
|
|
||||||
return &util.JSONResponse{
|
return &util.JSONResponse{
|
||||||
Code: http.StatusBadRequest,
|
Code: http.StatusBadRequest,
|
||||||
JSON: jsonerror.BadJSON("user id must be in the form @localpart:domain"),
|
JSON: spec.BadJSON("user id must be in the form @localpart:domain"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
switch r.Preset {
|
switch r.Preset {
|
||||||
case presetPrivateChat, presetTrustedPrivateChat, presetPublicChat, "":
|
case spec.PresetPrivateChat, spec.PresetTrustedPrivateChat, spec.PresetPublicChat, "":
|
||||||
default:
|
default:
|
||||||
return &util.JSONResponse{
|
return &util.JSONResponse{
|
||||||
Code: http.StatusBadRequest,
|
Code: http.StatusBadRequest,
|
||||||
JSON: jsonerror.BadJSON("preset must be any of 'private_chat', 'trusted_private_chat', 'public_chat'"),
|
JSON: spec.BadJSON("preset must be any of 'private_chat', 'trusted_private_chat', 'public_chat'"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -105,7 +85,7 @@ func (r createRoomRequest) Validate() *util.JSONResponse {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return &util.JSONResponse{
|
return &util.JSONResponse{
|
||||||
Code: http.StatusBadRequest,
|
Code: http.StatusBadRequest,
|
||||||
JSON: jsonerror.BadJSON("malformed creation_content"),
|
JSON: spec.BadJSON("malformed creation_content"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -114,7 +94,7 @@ func (r createRoomRequest) Validate() *util.JSONResponse {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return &util.JSONResponse{
|
return &util.JSONResponse{
|
||||||
Code: http.StatusBadRequest,
|
Code: http.StatusBadRequest,
|
||||||
JSON: jsonerror.BadJSON("malformed creation_content"),
|
JSON: spec.BadJSON("malformed creation_content"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -127,13 +107,6 @@ type createRoomResponse struct {
|
||||||
RoomAlias string `json:"room_alias,omitempty"` // in synapse not spec
|
RoomAlias string `json:"room_alias,omitempty"` // in synapse not spec
|
||||||
}
|
}
|
||||||
|
|
||||||
// fledglingEvent is a helper representation of an event used when creating many events in succession.
|
|
||||||
type fledglingEvent struct {
|
|
||||||
Type string `json:"type"`
|
|
||||||
StateKey string `json:"state_key"`
|
|
||||||
Content interface{} `json:"content"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// CreateRoom implements /createRoom
|
// CreateRoom implements /createRoom
|
||||||
func CreateRoom(
|
func CreateRoom(
|
||||||
req *http.Request, device *api.Device,
|
req *http.Request, device *api.Device,
|
||||||
|
@ -141,456 +114,124 @@ func CreateRoom(
|
||||||
profileAPI api.ClientUserAPI, rsAPI roomserverAPI.ClientRoomserverAPI,
|
profileAPI api.ClientUserAPI, rsAPI roomserverAPI.ClientRoomserverAPI,
|
||||||
asAPI appserviceAPI.AppServiceInternalAPI,
|
asAPI appserviceAPI.AppServiceInternalAPI,
|
||||||
) util.JSONResponse {
|
) util.JSONResponse {
|
||||||
var r createRoomRequest
|
var createRequest createRoomRequest
|
||||||
resErr := httputil.UnmarshalJSONRequest(req, &r)
|
resErr := httputil.UnmarshalJSONRequest(req, &createRequest)
|
||||||
if resErr != nil {
|
if resErr != nil {
|
||||||
return *resErr
|
return *resErr
|
||||||
}
|
}
|
||||||
if resErr = r.Validate(); resErr != nil {
|
if resErr = createRequest.Validate(); resErr != nil {
|
||||||
return *resErr
|
return *resErr
|
||||||
}
|
}
|
||||||
evTime, err := httputil.ParseTSParam(req)
|
evTime, err := httputil.ParseTSParam(req)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return util.JSONResponse{
|
return util.JSONResponse{
|
||||||
Code: http.StatusBadRequest,
|
Code: http.StatusBadRequest,
|
||||||
JSON: jsonerror.InvalidArgumentValue(err.Error()),
|
JSON: spec.InvalidParam(err.Error()),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return createRoom(req.Context(), r, device, cfg, profileAPI, rsAPI, asAPI, evTime)
|
return createRoom(req.Context(), createRequest, device, cfg, profileAPI, rsAPI, asAPI, evTime)
|
||||||
}
|
}
|
||||||
|
|
||||||
// createRoom implements /createRoom
|
// createRoom implements /createRoom
|
||||||
// nolint: gocyclo
|
|
||||||
func createRoom(
|
func createRoom(
|
||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
r createRoomRequest, device *api.Device,
|
createRequest createRoomRequest, device *api.Device,
|
||||||
cfg *config.ClientAPI,
|
cfg *config.ClientAPI,
|
||||||
profileAPI api.ClientUserAPI, rsAPI roomserverAPI.ClientRoomserverAPI,
|
profileAPI api.ClientUserAPI, rsAPI roomserverAPI.ClientRoomserverAPI,
|
||||||
asAPI appserviceAPI.AppServiceInternalAPI,
|
asAPI appserviceAPI.AppServiceInternalAPI,
|
||||||
evTime time.Time,
|
evTime time.Time,
|
||||||
) util.JSONResponse {
|
) util.JSONResponse {
|
||||||
_, userDomain, err := gomatrixserverlib.SplitID('@', device.UserID)
|
userID, err := spec.NewUserID(device.UserID, true)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
util.GetLogger(ctx).WithError(err).Error("gomatrixserverlib.SplitID failed")
|
util.GetLogger(ctx).WithError(err).Error("invalid userID")
|
||||||
return jsonerror.InternalServerError()
|
return util.JSONResponse{
|
||||||
|
Code: http.StatusInternalServerError,
|
||||||
|
JSON: spec.InternalServerError{},
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if !cfg.Matrix.IsLocalServerName(userDomain) {
|
if !cfg.Matrix.IsLocalServerName(userID.Domain()) {
|
||||||
return util.JSONResponse{
|
return util.JSONResponse{
|
||||||
Code: http.StatusForbidden,
|
Code: http.StatusForbidden,
|
||||||
JSON: jsonerror.Forbidden(fmt.Sprintf("User domain %q not configured locally", userDomain)),
|
JSON: spec.Forbidden(fmt.Sprintf("User domain %q not configured locally", userID.Domain())),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO (#267): Check room ID doesn't clash with an existing one, and we
|
|
||||||
// probably shouldn't be using pseudo-random strings, maybe GUIDs?
|
|
||||||
roomID := fmt.Sprintf("!%s:%s", util.RandomString(16), userDomain)
|
|
||||||
|
|
||||||
logger := util.GetLogger(ctx)
|
logger := util.GetLogger(ctx)
|
||||||
userID := device.UserID
|
|
||||||
|
// TODO: Check room ID doesn't clash with an existing one, and we
|
||||||
|
// probably shouldn't be using pseudo-random strings, maybe GUIDs?
|
||||||
|
roomID, err := spec.NewRoomID(fmt.Sprintf("!%s:%s", util.RandomString(16), userID.Domain()))
|
||||||
|
if err != nil {
|
||||||
|
util.GetLogger(ctx).WithError(err).Error("invalid roomID")
|
||||||
|
return util.JSONResponse{
|
||||||
|
Code: http.StatusInternalServerError,
|
||||||
|
JSON: spec.InternalServerError{},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Clobber keys: creator, room_version
|
// Clobber keys: creator, room_version
|
||||||
|
|
||||||
roomVersion := roomserverVersion.DefaultRoomVersion()
|
roomVersion := rsAPI.DefaultRoomVersion()
|
||||||
if r.RoomVersion != "" {
|
if createRequest.RoomVersion != "" {
|
||||||
candidateVersion := gomatrixserverlib.RoomVersion(r.RoomVersion)
|
candidateVersion := gomatrixserverlib.RoomVersion(createRequest.RoomVersion)
|
||||||
_, roomVersionError := roomserverVersion.SupportedRoomVersion(candidateVersion)
|
_, roomVersionError := roomserverVersion.SupportedRoomVersion(candidateVersion)
|
||||||
if roomVersionError != nil {
|
if roomVersionError != nil {
|
||||||
return util.JSONResponse{
|
return util.JSONResponse{
|
||||||
Code: http.StatusBadRequest,
|
Code: http.StatusBadRequest,
|
||||||
JSON: jsonerror.UnsupportedRoomVersion(roomVersionError.Error()),
|
JSON: spec.UnsupportedRoomVersion(roomVersionError.Error()),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
roomVersion = candidateVersion
|
roomVersion = candidateVersion
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: visibility/presets/raw initial state
|
|
||||||
// TODO: Create room alias association
|
|
||||||
// Make sure this doesn't fall into an application service's namespace though!
|
|
||||||
|
|
||||||
logger.WithFields(log.Fields{
|
logger.WithFields(log.Fields{
|
||||||
"userID": userID,
|
"userID": userID.String(),
|
||||||
"roomID": roomID,
|
"roomID": roomID.String(),
|
||||||
"roomVersion": roomVersion,
|
"roomVersion": roomVersion,
|
||||||
}).Info("Creating new room")
|
}).Info("Creating new room")
|
||||||
|
|
||||||
profile, err := appserviceAPI.RetrieveUserProfile(ctx, userID, asAPI, profileAPI)
|
profile, err := appserviceAPI.RetrieveUserProfile(ctx, userID.String(), asAPI, profileAPI)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
util.GetLogger(ctx).WithError(err).Error("appserviceAPI.RetrieveUserProfile failed")
|
util.GetLogger(ctx).WithError(err).Error("appserviceAPI.RetrieveUserProfile failed")
|
||||||
return jsonerror.InternalServerError()
|
return util.JSONResponse{
|
||||||
}
|
Code: http.StatusInternalServerError,
|
||||||
|
JSON: spec.InternalServerError{},
|
||||||
createContent := map[string]interface{}{}
|
|
||||||
if len(r.CreationContent) > 0 {
|
|
||||||
if err = json.Unmarshal(r.CreationContent, &createContent); err != nil {
|
|
||||||
util.GetLogger(ctx).WithError(err).Error("json.Unmarshal for creation_content failed")
|
|
||||||
return util.JSONResponse{
|
|
||||||
Code: http.StatusBadRequest,
|
|
||||||
JSON: jsonerror.BadJSON("invalid create content"),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
createContent["creator"] = userID
|
|
||||||
createContent["room_version"] = roomVersion
|
|
||||||
powerLevelContent := eventutil.InitialPowerLevelsContent(userID)
|
|
||||||
joinRuleContent := gomatrixserverlib.JoinRuleContent{
|
|
||||||
JoinRule: gomatrixserverlib.Invite,
|
|
||||||
}
|
|
||||||
historyVisibilityContent := gomatrixserverlib.HistoryVisibilityContent{
|
|
||||||
HistoryVisibility: historyVisibilityShared,
|
|
||||||
}
|
|
||||||
|
|
||||||
if r.PowerLevelContentOverride != nil {
|
|
||||||
// Merge powerLevelContentOverride fields by unmarshalling it atop the defaults
|
|
||||||
err = json.Unmarshal(r.PowerLevelContentOverride, &powerLevelContent)
|
|
||||||
if err != nil {
|
|
||||||
util.GetLogger(ctx).WithError(err).Error("json.Unmarshal for power_level_content_override failed")
|
|
||||||
return util.JSONResponse{
|
|
||||||
Code: http.StatusBadRequest,
|
|
||||||
JSON: jsonerror.BadJSON("malformed power_level_content_override"),
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
switch r.Preset {
|
userDisplayName := profile.DisplayName
|
||||||
case presetPrivateChat:
|
userAvatarURL := profile.AvatarURL
|
||||||
joinRuleContent.JoinRule = gomatrixserverlib.Invite
|
|
||||||
historyVisibilityContent.HistoryVisibility = historyVisibilityShared
|
keyID := cfg.Matrix.KeyID
|
||||||
case presetTrustedPrivateChat:
|
privateKey := cfg.Matrix.PrivateKey
|
||||||
joinRuleContent.JoinRule = gomatrixserverlib.Invite
|
|
||||||
historyVisibilityContent.HistoryVisibility = historyVisibilityShared
|
req := roomserverAPI.PerformCreateRoomRequest{
|
||||||
for _, invitee := range r.Invite {
|
InvitedUsers: createRequest.Invite,
|
||||||
powerLevelContent.Users[invitee] = 100
|
RoomName: createRequest.Name,
|
||||||
}
|
Visibility: createRequest.Visibility,
|
||||||
case presetPublicChat:
|
Topic: createRequest.Topic,
|
||||||
joinRuleContent.JoinRule = gomatrixserverlib.Public
|
StatePreset: createRequest.Preset,
|
||||||
historyVisibilityContent.HistoryVisibility = historyVisibilityShared
|
CreationContent: createRequest.CreationContent,
|
||||||
|
InitialState: createRequest.InitialState,
|
||||||
|
RoomAliasName: createRequest.RoomAliasName,
|
||||||
|
RoomVersion: roomVersion,
|
||||||
|
PowerLevelContentOverride: createRequest.PowerLevelContentOverride,
|
||||||
|
IsDirect: createRequest.IsDirect,
|
||||||
|
|
||||||
|
UserDisplayName: userDisplayName,
|
||||||
|
UserAvatarURL: userAvatarURL,
|
||||||
|
KeyID: keyID,
|
||||||
|
PrivateKey: privateKey,
|
||||||
|
EventTime: evTime,
|
||||||
}
|
}
|
||||||
|
|
||||||
createEvent := fledglingEvent{
|
roomAlias, createRes := rsAPI.PerformCreateRoom(ctx, *userID, *roomID, &req)
|
||||||
Type: gomatrixserverlib.MRoomCreate,
|
if createRes != nil {
|
||||||
Content: createContent,
|
return *createRes
|
||||||
}
|
|
||||||
powerLevelEvent := fledglingEvent{
|
|
||||||
Type: gomatrixserverlib.MRoomPowerLevels,
|
|
||||||
Content: powerLevelContent,
|
|
||||||
}
|
|
||||||
joinRuleEvent := fledglingEvent{
|
|
||||||
Type: gomatrixserverlib.MRoomJoinRules,
|
|
||||||
Content: joinRuleContent,
|
|
||||||
}
|
|
||||||
historyVisibilityEvent := fledglingEvent{
|
|
||||||
Type: gomatrixserverlib.MRoomHistoryVisibility,
|
|
||||||
Content: historyVisibilityContent,
|
|
||||||
}
|
|
||||||
membershipEvent := fledglingEvent{
|
|
||||||
Type: gomatrixserverlib.MRoomMember,
|
|
||||||
StateKey: userID,
|
|
||||||
Content: gomatrixserverlib.MemberContent{
|
|
||||||
Membership: gomatrixserverlib.Join,
|
|
||||||
DisplayName: profile.DisplayName,
|
|
||||||
AvatarURL: profile.AvatarURL,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
var nameEvent *fledglingEvent
|
|
||||||
var topicEvent *fledglingEvent
|
|
||||||
var guestAccessEvent *fledglingEvent
|
|
||||||
var aliasEvent *fledglingEvent
|
|
||||||
|
|
||||||
if r.Name != "" {
|
|
||||||
nameEvent = &fledglingEvent{
|
|
||||||
Type: gomatrixserverlib.MRoomName,
|
|
||||||
Content: eventutil.NameContent{
|
|
||||||
Name: r.Name,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if r.Topic != "" {
|
|
||||||
topicEvent = &fledglingEvent{
|
|
||||||
Type: gomatrixserverlib.MRoomTopic,
|
|
||||||
Content: eventutil.TopicContent{
|
|
||||||
Topic: r.Topic,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if r.GuestCanJoin {
|
|
||||||
guestAccessEvent = &fledglingEvent{
|
|
||||||
Type: gomatrixserverlib.MRoomGuestAccess,
|
|
||||||
Content: eventutil.GuestAccessContent{
|
|
||||||
GuestAccess: "can_join",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var roomAlias string
|
|
||||||
if r.RoomAliasName != "" {
|
|
||||||
roomAlias = fmt.Sprintf("#%s:%s", r.RoomAliasName, userDomain)
|
|
||||||
// check it's free TODO: This races but is better than nothing
|
|
||||||
hasAliasReq := roomserverAPI.GetRoomIDForAliasRequest{
|
|
||||||
Alias: roomAlias,
|
|
||||||
IncludeAppservices: false,
|
|
||||||
}
|
|
||||||
|
|
||||||
var aliasResp roomserverAPI.GetRoomIDForAliasResponse
|
|
||||||
err = rsAPI.GetRoomIDForAlias(ctx, &hasAliasReq, &aliasResp)
|
|
||||||
if err != nil {
|
|
||||||
util.GetLogger(ctx).WithError(err).Error("aliasAPI.GetRoomIDForAlias failed")
|
|
||||||
return jsonerror.InternalServerError()
|
|
||||||
}
|
|
||||||
if aliasResp.RoomID != "" {
|
|
||||||
return util.JSONResponse{
|
|
||||||
Code: http.StatusBadRequest,
|
|
||||||
JSON: jsonerror.RoomInUse("Room ID already exists."),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
aliasEvent = &fledglingEvent{
|
|
||||||
Type: gomatrixserverlib.MRoomCanonicalAlias,
|
|
||||||
Content: eventutil.CanonicalAlias{
|
|
||||||
Alias: roomAlias,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var initialStateEvents []fledglingEvent
|
|
||||||
for i := range r.InitialState {
|
|
||||||
if r.InitialState[i].StateKey != "" {
|
|
||||||
initialStateEvents = append(initialStateEvents, r.InitialState[i])
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
switch r.InitialState[i].Type {
|
|
||||||
case gomatrixserverlib.MRoomCreate:
|
|
||||||
continue
|
|
||||||
|
|
||||||
case gomatrixserverlib.MRoomPowerLevels:
|
|
||||||
powerLevelEvent = r.InitialState[i]
|
|
||||||
|
|
||||||
case gomatrixserverlib.MRoomJoinRules:
|
|
||||||
joinRuleEvent = r.InitialState[i]
|
|
||||||
|
|
||||||
case gomatrixserverlib.MRoomHistoryVisibility:
|
|
||||||
historyVisibilityEvent = r.InitialState[i]
|
|
||||||
|
|
||||||
case gomatrixserverlib.MRoomGuestAccess:
|
|
||||||
guestAccessEvent = &r.InitialState[i]
|
|
||||||
|
|
||||||
case gomatrixserverlib.MRoomName:
|
|
||||||
nameEvent = &r.InitialState[i]
|
|
||||||
|
|
||||||
case gomatrixserverlib.MRoomTopic:
|
|
||||||
topicEvent = &r.InitialState[i]
|
|
||||||
|
|
||||||
default:
|
|
||||||
initialStateEvents = append(initialStateEvents, r.InitialState[i])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// send events into the room in order of:
|
|
||||||
// 1- m.room.create
|
|
||||||
// 2- room creator join member
|
|
||||||
// 3- m.room.power_levels
|
|
||||||
// 4- m.room.join_rules
|
|
||||||
// 5- m.room.history_visibility
|
|
||||||
// 6- m.room.canonical_alias (opt)
|
|
||||||
// 7- m.room.guest_access (opt)
|
|
||||||
// 8- other initial state items
|
|
||||||
// 9- m.room.name (opt)
|
|
||||||
// 10- m.room.topic (opt)
|
|
||||||
// 11- invite events (opt) - with is_direct flag if applicable TODO
|
|
||||||
// 12- 3pid invite events (opt) TODO
|
|
||||||
// This differs from Synapse slightly. Synapse would vary the ordering of 3-7
|
|
||||||
// depending on if those events were in "initial_state" or not. This made it
|
|
||||||
// harder to reason about, hence sticking to a strict static ordering.
|
|
||||||
// TODO: Synapse has txn/token ID on each event. Do we need to do this here?
|
|
||||||
eventsToMake := []fledglingEvent{
|
|
||||||
createEvent, membershipEvent, powerLevelEvent, joinRuleEvent, historyVisibilityEvent,
|
|
||||||
}
|
|
||||||
if guestAccessEvent != nil {
|
|
||||||
eventsToMake = append(eventsToMake, *guestAccessEvent)
|
|
||||||
}
|
|
||||||
eventsToMake = append(eventsToMake, initialStateEvents...)
|
|
||||||
if nameEvent != nil {
|
|
||||||
eventsToMake = append(eventsToMake, *nameEvent)
|
|
||||||
}
|
|
||||||
if topicEvent != nil {
|
|
||||||
eventsToMake = append(eventsToMake, *topicEvent)
|
|
||||||
}
|
|
||||||
if aliasEvent != nil {
|
|
||||||
// TODO: bit of a chicken and egg problem here as the alias doesn't exist and cannot until we have made the room.
|
|
||||||
// This means we might fail creating the alias but say the canonical alias is something that doesn't exist.
|
|
||||||
eventsToMake = append(eventsToMake, *aliasEvent)
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: invite events
|
|
||||||
// TODO: 3pid invite events
|
|
||||||
|
|
||||||
var builtEvents []*gomatrixserverlib.HeaderedEvent
|
|
||||||
authEvents := gomatrixserverlib.NewAuthEvents(nil)
|
|
||||||
for i, e := range eventsToMake {
|
|
||||||
depth := i + 1 // depth starts at 1
|
|
||||||
|
|
||||||
builder := gomatrixserverlib.EventBuilder{
|
|
||||||
Sender: userID,
|
|
||||||
RoomID: roomID,
|
|
||||||
Type: e.Type,
|
|
||||||
StateKey: &e.StateKey,
|
|
||||||
Depth: int64(depth),
|
|
||||||
}
|
|
||||||
err = builder.SetContent(e.Content)
|
|
||||||
if err != nil {
|
|
||||||
util.GetLogger(ctx).WithError(err).Error("builder.SetContent failed")
|
|
||||||
return jsonerror.InternalServerError()
|
|
||||||
}
|
|
||||||
if i > 0 {
|
|
||||||
builder.PrevEvents = []gomatrixserverlib.EventReference{builtEvents[i-1].EventReference()}
|
|
||||||
}
|
|
||||||
var ev *gomatrixserverlib.Event
|
|
||||||
ev, err = buildEvent(&builder, userDomain, &authEvents, cfg, evTime, roomVersion)
|
|
||||||
if err != nil {
|
|
||||||
util.GetLogger(ctx).WithError(err).Error("buildEvent failed")
|
|
||||||
return jsonerror.InternalServerError()
|
|
||||||
}
|
|
||||||
|
|
||||||
if err = gomatrixserverlib.Allowed(ev, &authEvents); err != nil {
|
|
||||||
util.GetLogger(ctx).WithError(err).Error("gomatrixserverlib.Allowed failed")
|
|
||||||
return jsonerror.InternalServerError()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add the event to the list of auth events
|
|
||||||
builtEvents = append(builtEvents, ev.Headered(roomVersion))
|
|
||||||
err = authEvents.AddEvent(ev)
|
|
||||||
if err != nil {
|
|
||||||
util.GetLogger(ctx).WithError(err).Error("authEvents.AddEvent failed")
|
|
||||||
return jsonerror.InternalServerError()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
inputs := make([]roomserverAPI.InputRoomEvent, 0, len(builtEvents))
|
|
||||||
for _, event := range builtEvents {
|
|
||||||
inputs = append(inputs, roomserverAPI.InputRoomEvent{
|
|
||||||
Kind: roomserverAPI.KindNew,
|
|
||||||
Event: event,
|
|
||||||
Origin: userDomain,
|
|
||||||
SendAsServer: roomserverAPI.DoNotSendToOtherServers,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
if err = roomserverAPI.SendInputRoomEvents(ctx, rsAPI, device.UserDomain(), inputs, false); err != nil {
|
|
||||||
util.GetLogger(ctx).WithError(err).Error("roomserverAPI.SendInputRoomEvents failed")
|
|
||||||
return jsonerror.InternalServerError()
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO(#269): Reserve room alias while we create the room. This stops us
|
|
||||||
// from creating the room but still failing due to the alias having already
|
|
||||||
// been taken.
|
|
||||||
if roomAlias != "" {
|
|
||||||
aliasReq := roomserverAPI.SetRoomAliasRequest{
|
|
||||||
Alias: roomAlias,
|
|
||||||
RoomID: roomID,
|
|
||||||
UserID: userID,
|
|
||||||
}
|
|
||||||
|
|
||||||
var aliasResp roomserverAPI.SetRoomAliasResponse
|
|
||||||
err = rsAPI.SetRoomAlias(ctx, &aliasReq, &aliasResp)
|
|
||||||
if err != nil {
|
|
||||||
util.GetLogger(ctx).WithError(err).Error("aliasAPI.SetRoomAlias failed")
|
|
||||||
return jsonerror.InternalServerError()
|
|
||||||
}
|
|
||||||
|
|
||||||
if aliasResp.AliasExists {
|
|
||||||
return util.JSONResponse{
|
|
||||||
Code: http.StatusBadRequest,
|
|
||||||
JSON: jsonerror.RoomInUse("Room alias already exists."),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// If this is a direct message then we should invite the participants.
|
|
||||||
if len(r.Invite) > 0 {
|
|
||||||
// Build some stripped state for the invite.
|
|
||||||
var globalStrippedState []gomatrixserverlib.InviteV2StrippedState
|
|
||||||
for _, event := range builtEvents {
|
|
||||||
// Chosen events from the spec:
|
|
||||||
// https://spec.matrix.org/v1.3/client-server-api/#stripped-state
|
|
||||||
switch event.Type() {
|
|
||||||
case gomatrixserverlib.MRoomCreate:
|
|
||||||
fallthrough
|
|
||||||
case gomatrixserverlib.MRoomName:
|
|
||||||
fallthrough
|
|
||||||
case gomatrixserverlib.MRoomAvatar:
|
|
||||||
fallthrough
|
|
||||||
case gomatrixserverlib.MRoomTopic:
|
|
||||||
fallthrough
|
|
||||||
case gomatrixserverlib.MRoomCanonicalAlias:
|
|
||||||
fallthrough
|
|
||||||
case gomatrixserverlib.MRoomEncryption:
|
|
||||||
fallthrough
|
|
||||||
case gomatrixserverlib.MRoomMember:
|
|
||||||
fallthrough
|
|
||||||
case gomatrixserverlib.MRoomJoinRules:
|
|
||||||
ev := event.Event
|
|
||||||
globalStrippedState = append(
|
|
||||||
globalStrippedState,
|
|
||||||
gomatrixserverlib.NewInviteV2StrippedState(ev),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Process the invites.
|
|
||||||
for _, invitee := range r.Invite {
|
|
||||||
// Build the invite event.
|
|
||||||
inviteEvent, err := buildMembershipEvent(
|
|
||||||
ctx, invitee, "", profileAPI, device, gomatrixserverlib.Invite,
|
|
||||||
roomID, r.IsDirect, cfg, evTime, rsAPI, asAPI,
|
|
||||||
)
|
|
||||||
if err != nil {
|
|
||||||
util.GetLogger(ctx).WithError(err).Error("buildMembershipEvent failed")
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
inviteStrippedState := append(
|
|
||||||
globalStrippedState,
|
|
||||||
gomatrixserverlib.NewInviteV2StrippedState(inviteEvent.Event),
|
|
||||||
)
|
|
||||||
// Send the invite event to the roomserver.
|
|
||||||
var inviteRes roomserverAPI.PerformInviteResponse
|
|
||||||
event := inviteEvent.Headered(roomVersion)
|
|
||||||
if err := rsAPI.PerformInvite(ctx, &roomserverAPI.PerformInviteRequest{
|
|
||||||
Event: event,
|
|
||||||
InviteRoomState: inviteStrippedState,
|
|
||||||
RoomVersion: event.RoomVersion,
|
|
||||||
SendAsServer: string(userDomain),
|
|
||||||
}, &inviteRes); err != nil {
|
|
||||||
util.GetLogger(ctx).WithError(err).Error("PerformInvite failed")
|
|
||||||
return util.JSONResponse{
|
|
||||||
Code: http.StatusInternalServerError,
|
|
||||||
JSON: jsonerror.InternalServerError(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if inviteRes.Error != nil {
|
|
||||||
return inviteRes.Error.JSONResponse()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if r.Visibility == "public" {
|
|
||||||
// expose this room in the published room list
|
|
||||||
var pubRes roomserverAPI.PerformPublishResponse
|
|
||||||
if err := rsAPI.PerformPublish(ctx, &roomserverAPI.PerformPublishRequest{
|
|
||||||
RoomID: roomID,
|
|
||||||
Visibility: "public",
|
|
||||||
}, &pubRes); err != nil {
|
|
||||||
return jsonerror.InternalAPIError(ctx, err)
|
|
||||||
}
|
|
||||||
if pubRes.Error != nil {
|
|
||||||
// treat as non-fatal since the room is already made by this point
|
|
||||||
util.GetLogger(ctx).WithError(pubRes.Error).Error("failed to visibility:public")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
response := createRoomResponse{
|
response := createRoomResponse{
|
||||||
RoomID: roomID,
|
RoomID: roomID.String(),
|
||||||
RoomAlias: roomAlias,
|
RoomAlias: roomAlias,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -599,31 +240,3 @@ func createRoom(
|
||||||
JSON: response,
|
JSON: response,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// buildEvent fills out auth_events for the builder then builds the event
|
|
||||||
func buildEvent(
|
|
||||||
builder *gomatrixserverlib.EventBuilder,
|
|
||||||
serverName gomatrixserverlib.ServerName,
|
|
||||||
provider gomatrixserverlib.AuthEventProvider,
|
|
||||||
cfg *config.ClientAPI,
|
|
||||||
evTime time.Time,
|
|
||||||
roomVersion gomatrixserverlib.RoomVersion,
|
|
||||||
) (*gomatrixserverlib.Event, error) {
|
|
||||||
eventsNeeded, err := gomatrixserverlib.StateNeededForEventBuilder(builder)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
refs, err := eventsNeeded.AuthEventReferences(provider)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
builder.AuthEvents = refs
|
|
||||||
event, err := builder.Build(
|
|
||||||
evTime, serverName, cfg.Matrix.KeyID,
|
|
||||||
cfg.Matrix.PrivateKey, roomVersion,
|
|
||||||
)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("cannot build event %s : Builder failed to build. %w", builder.Type, err)
|
|
||||||
}
|
|
||||||
return event, nil
|
|
||||||
}
|
|
||||||
|
|
|
@ -5,9 +5,9 @@ import (
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
"github.com/matrix-org/dendrite/clientapi/auth"
|
"github.com/matrix-org/dendrite/clientapi/auth"
|
||||||
"github.com/matrix-org/dendrite/clientapi/jsonerror"
|
|
||||||
"github.com/matrix-org/dendrite/userapi/api"
|
"github.com/matrix-org/dendrite/userapi/api"
|
||||||
"github.com/matrix-org/gomatrixserverlib"
|
"github.com/matrix-org/gomatrixserverlib"
|
||||||
|
"github.com/matrix-org/gomatrixserverlib/spec"
|
||||||
"github.com/matrix-org/util"
|
"github.com/matrix-org/util"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -24,7 +24,7 @@ func Deactivate(
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return util.JSONResponse{
|
return util.JSONResponse{
|
||||||
Code: http.StatusBadRequest,
|
Code: http.StatusBadRequest,
|
||||||
JSON: jsonerror.BadJSON("The request body could not be read: " + err.Error()),
|
JSON: spec.BadJSON("The request body could not be read: " + err.Error()),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -33,19 +33,26 @@ func Deactivate(
|
||||||
return *errRes
|
return *errRes
|
||||||
}
|
}
|
||||||
|
|
||||||
localpart, _, err := gomatrixserverlib.SplitID('@', login.Username())
|
localpart, serverName, err := gomatrixserverlib.SplitID('@', login.Username())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
util.GetLogger(req.Context()).WithError(err).Error("gomatrixserverlib.SplitID failed")
|
util.GetLogger(req.Context()).WithError(err).Error("gomatrixserverlib.SplitID failed")
|
||||||
return jsonerror.InternalServerError()
|
return util.JSONResponse{
|
||||||
|
Code: http.StatusInternalServerError,
|
||||||
|
JSON: spec.InternalServerError{},
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var res api.PerformAccountDeactivationResponse
|
var res api.PerformAccountDeactivationResponse
|
||||||
err = accountAPI.PerformAccountDeactivation(ctx, &api.PerformAccountDeactivationRequest{
|
err = accountAPI.PerformAccountDeactivation(ctx, &api.PerformAccountDeactivationRequest{
|
||||||
Localpart: localpart,
|
Localpart: localpart,
|
||||||
|
ServerName: serverName,
|
||||||
}, &res)
|
}, &res)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
util.GetLogger(ctx).WithError(err).Error("userAPI.PerformAccountDeactivation failed")
|
util.GetLogger(ctx).WithError(err).Error("userAPI.PerformAccountDeactivation failed")
|
||||||
return jsonerror.InternalServerError()
|
return util.JSONResponse{
|
||||||
|
Code: http.StatusInternalServerError,
|
||||||
|
JSON: spec.InternalServerError{},
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return util.JSONResponse{
|
return util.JSONResponse{
|
||||||
|
|
|
@ -15,15 +15,16 @@
|
||||||
package routing
|
package routing
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"encoding/json"
|
||||||
"io"
|
"io"
|
||||||
"net"
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
"github.com/matrix-org/dendrite/clientapi/auth"
|
"github.com/matrix-org/dendrite/clientapi/auth"
|
||||||
"github.com/matrix-org/dendrite/clientapi/httputil"
|
"github.com/matrix-org/dendrite/clientapi/httputil"
|
||||||
"github.com/matrix-org/dendrite/clientapi/jsonerror"
|
|
||||||
"github.com/matrix-org/dendrite/userapi/api"
|
"github.com/matrix-org/dendrite/userapi/api"
|
||||||
"github.com/matrix-org/gomatrixserverlib"
|
"github.com/matrix-org/gomatrixserverlib"
|
||||||
|
"github.com/matrix-org/gomatrixserverlib/spec"
|
||||||
"github.com/matrix-org/util"
|
"github.com/matrix-org/util"
|
||||||
"github.com/tidwall/gjson"
|
"github.com/tidwall/gjson"
|
||||||
)
|
)
|
||||||
|
@ -59,7 +60,10 @@ func GetDeviceByID(
|
||||||
}, &queryRes)
|
}, &queryRes)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
util.GetLogger(req.Context()).WithError(err).Error("QueryDevices failed")
|
util.GetLogger(req.Context()).WithError(err).Error("QueryDevices failed")
|
||||||
return jsonerror.InternalServerError()
|
return util.JSONResponse{
|
||||||
|
Code: http.StatusInternalServerError,
|
||||||
|
JSON: spec.InternalServerError{},
|
||||||
|
}
|
||||||
}
|
}
|
||||||
var targetDevice *api.Device
|
var targetDevice *api.Device
|
||||||
for _, device := range queryRes.Devices {
|
for _, device := range queryRes.Devices {
|
||||||
|
@ -71,7 +75,7 @@ func GetDeviceByID(
|
||||||
if targetDevice == nil {
|
if targetDevice == nil {
|
||||||
return util.JSONResponse{
|
return util.JSONResponse{
|
||||||
Code: http.StatusNotFound,
|
Code: http.StatusNotFound,
|
||||||
JSON: jsonerror.NotFound("Unknown device"),
|
JSON: spec.NotFound("Unknown device"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -96,7 +100,10 @@ func GetDevicesByLocalpart(
|
||||||
}, &queryRes)
|
}, &queryRes)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
util.GetLogger(req.Context()).WithError(err).Error("QueryDevices failed")
|
util.GetLogger(req.Context()).WithError(err).Error("QueryDevices failed")
|
||||||
return jsonerror.InternalServerError()
|
return util.JSONResponse{
|
||||||
|
Code: http.StatusInternalServerError,
|
||||||
|
JSON: spec.InternalServerError{},
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
res := devicesJSON{}
|
res := devicesJSON{}
|
||||||
|
@ -138,18 +145,15 @@ func UpdateDeviceByID(
|
||||||
}, &performRes)
|
}, &performRes)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
util.GetLogger(req.Context()).WithError(err).Error("PerformDeviceUpdate failed")
|
util.GetLogger(req.Context()).WithError(err).Error("PerformDeviceUpdate failed")
|
||||||
return jsonerror.InternalServerError()
|
return util.JSONResponse{
|
||||||
|
Code: http.StatusInternalServerError,
|
||||||
|
JSON: spec.InternalServerError{},
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if !performRes.DeviceExists {
|
if !performRes.DeviceExists {
|
||||||
return util.JSONResponse{
|
return util.JSONResponse{
|
||||||
Code: http.StatusNotFound,
|
Code: http.StatusNotFound,
|
||||||
JSON: jsonerror.Forbidden("device does not exist"),
|
JSON: spec.Forbidden("device does not exist"),
|
||||||
}
|
|
||||||
}
|
|
||||||
if performRes.Forbidden {
|
|
||||||
return util.JSONResponse{
|
|
||||||
Code: http.StatusForbidden,
|
|
||||||
JSON: jsonerror.Forbidden("device not owned by current user"),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -179,7 +183,7 @@ func DeleteDeviceById(
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return util.JSONResponse{
|
return util.JSONResponse{
|
||||||
Code: http.StatusBadRequest,
|
Code: http.StatusBadRequest,
|
||||||
JSON: jsonerror.BadJSON("The request body could not be read: " + err.Error()),
|
JSON: spec.BadJSON("The request body could not be read: " + err.Error()),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -189,7 +193,7 @@ func DeleteDeviceById(
|
||||||
if dev != deviceID {
|
if dev != deviceID {
|
||||||
return util.JSONResponse{
|
return util.JSONResponse{
|
||||||
Code: http.StatusForbidden,
|
Code: http.StatusForbidden,
|
||||||
JSON: jsonerror.Forbidden("session & device mismatch"),
|
JSON: spec.Forbidden("session and device mismatch"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -211,7 +215,10 @@ func DeleteDeviceById(
|
||||||
localpart, _, err := gomatrixserverlib.SplitID('@', device.UserID)
|
localpart, _, err := gomatrixserverlib.SplitID('@', device.UserID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
util.GetLogger(ctx).WithError(err).Error("gomatrixserverlib.SplitID failed")
|
util.GetLogger(ctx).WithError(err).Error("gomatrixserverlib.SplitID failed")
|
||||||
return jsonerror.InternalServerError()
|
return util.JSONResponse{
|
||||||
|
Code: http.StatusInternalServerError,
|
||||||
|
JSON: spec.InternalServerError{},
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// make sure that the access token being used matches the login creds used for user interactive auth, else
|
// make sure that the access token being used matches the login creds used for user interactive auth, else
|
||||||
|
@ -219,7 +226,7 @@ func DeleteDeviceById(
|
||||||
if login.Username() != localpart && login.Username() != device.UserID {
|
if login.Username() != localpart && login.Username() != device.UserID {
|
||||||
return util.JSONResponse{
|
return util.JSONResponse{
|
||||||
Code: 403,
|
Code: 403,
|
||||||
JSON: jsonerror.Forbidden("Cannot delete another user's device"),
|
JSON: spec.Forbidden("Cannot delete another user's device"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -229,7 +236,10 @@ func DeleteDeviceById(
|
||||||
DeviceIDs: []string{deviceID},
|
DeviceIDs: []string{deviceID},
|
||||||
}, &res); err != nil {
|
}, &res); err != nil {
|
||||||
util.GetLogger(ctx).WithError(err).Error("userAPI.PerformDeviceDeletion failed")
|
util.GetLogger(ctx).WithError(err).Error("userAPI.PerformDeviceDeletion failed")
|
||||||
return jsonerror.InternalServerError()
|
return util.JSONResponse{
|
||||||
|
Code: http.StatusInternalServerError,
|
||||||
|
JSON: spec.InternalServerError{},
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
deleteOK = true
|
deleteOK = true
|
||||||
|
@ -242,16 +252,40 @@ func DeleteDeviceById(
|
||||||
|
|
||||||
// DeleteDevices handles POST requests to /delete_devices
|
// DeleteDevices handles POST requests to /delete_devices
|
||||||
func DeleteDevices(
|
func DeleteDevices(
|
||||||
req *http.Request, userAPI api.ClientUserAPI, device *api.Device,
|
req *http.Request, userInteractiveAuth *auth.UserInteractive, userAPI api.ClientUserAPI, device *api.Device,
|
||||||
) util.JSONResponse {
|
) util.JSONResponse {
|
||||||
ctx := req.Context()
|
ctx := req.Context()
|
||||||
payload := devicesDeleteJSON{}
|
|
||||||
|
|
||||||
if resErr := httputil.UnmarshalJSONRequest(req, &payload); resErr != nil {
|
bodyBytes, err := io.ReadAll(req.Body)
|
||||||
return *resErr
|
if err != nil {
|
||||||
|
return util.JSONResponse{
|
||||||
|
Code: http.StatusBadRequest,
|
||||||
|
JSON: spec.BadJSON("The request body could not be read: " + err.Error()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
defer req.Body.Close() // nolint:errcheck
|
||||||
|
|
||||||
|
// initiate UIA
|
||||||
|
login, errRes := userInteractiveAuth.Verify(ctx, bodyBytes, device)
|
||||||
|
if errRes != nil {
|
||||||
|
return *errRes
|
||||||
}
|
}
|
||||||
|
|
||||||
defer req.Body.Close() // nolint: errcheck
|
if login.Username() != device.UserID {
|
||||||
|
return util.JSONResponse{
|
||||||
|
Code: http.StatusForbidden,
|
||||||
|
JSON: spec.Forbidden("unable to delete devices for other user"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
payload := devicesDeleteJSON{}
|
||||||
|
if err = json.Unmarshal(bodyBytes, &payload); err != nil {
|
||||||
|
util.GetLogger(ctx).WithError(err).Error("unable to unmarshal device deletion request")
|
||||||
|
return util.JSONResponse{
|
||||||
|
Code: http.StatusInternalServerError,
|
||||||
|
JSON: spec.InternalServerError{},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
var res api.PerformDeviceDeletionResponse
|
var res api.PerformDeviceDeletionResponse
|
||||||
if err := userAPI.PerformDeviceDeletion(ctx, &api.PerformDeviceDeletionRequest{
|
if err := userAPI.PerformDeviceDeletion(ctx, &api.PerformDeviceDeletionRequest{
|
||||||
|
@ -259,7 +293,10 @@ func DeleteDevices(
|
||||||
DeviceIDs: payload.Devices,
|
DeviceIDs: payload.Devices,
|
||||||
}, &res); err != nil {
|
}, &res); err != nil {
|
||||||
util.GetLogger(ctx).WithError(err).Error("userAPI.PerformDeviceDeletion failed")
|
util.GetLogger(ctx).WithError(err).Error("userAPI.PerformDeviceDeletion failed")
|
||||||
return jsonerror.InternalServerError()
|
return util.JSONResponse{
|
||||||
|
Code: http.StatusInternalServerError,
|
||||||
|
JSON: spec.InternalServerError{},
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return util.JSONResponse{
|
return util.JSONResponse{
|
||||||
|
|
|
@ -19,10 +19,11 @@ import (
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
"github.com/matrix-org/gomatrixserverlib"
|
"github.com/matrix-org/gomatrixserverlib"
|
||||||
|
"github.com/matrix-org/gomatrixserverlib/fclient"
|
||||||
|
"github.com/matrix-org/gomatrixserverlib/spec"
|
||||||
"github.com/matrix-org/util"
|
"github.com/matrix-org/util"
|
||||||
|
|
||||||
"github.com/matrix-org/dendrite/clientapi/httputil"
|
"github.com/matrix-org/dendrite/clientapi/httputil"
|
||||||
"github.com/matrix-org/dendrite/clientapi/jsonerror"
|
|
||||||
federationAPI "github.com/matrix-org/dendrite/federationapi/api"
|
federationAPI "github.com/matrix-org/dendrite/federationapi/api"
|
||||||
roomserverAPI "github.com/matrix-org/dendrite/roomserver/api"
|
roomserverAPI "github.com/matrix-org/dendrite/roomserver/api"
|
||||||
"github.com/matrix-org/dendrite/setup/config"
|
"github.com/matrix-org/dendrite/setup/config"
|
||||||
|
@ -34,7 +35,7 @@ type roomDirectoryResponse struct {
|
||||||
Servers []string `json:"servers"`
|
Servers []string `json:"servers"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *roomDirectoryResponse) fillServers(servers []gomatrixserverlib.ServerName) {
|
func (r *roomDirectoryResponse) fillServers(servers []spec.ServerName) {
|
||||||
r.Servers = make([]string, len(servers))
|
r.Servers = make([]string, len(servers))
|
||||||
for i, s := range servers {
|
for i, s := range servers {
|
||||||
r.Servers[i] = string(s)
|
r.Servers[i] = string(s)
|
||||||
|
@ -45,7 +46,7 @@ func (r *roomDirectoryResponse) fillServers(servers []gomatrixserverlib.ServerNa
|
||||||
func DirectoryRoom(
|
func DirectoryRoom(
|
||||||
req *http.Request,
|
req *http.Request,
|
||||||
roomAlias string,
|
roomAlias string,
|
||||||
federation *gomatrixserverlib.FederationClient,
|
federation fclient.FederationClient,
|
||||||
cfg *config.ClientAPI,
|
cfg *config.ClientAPI,
|
||||||
rsAPI roomserverAPI.ClientRoomserverAPI,
|
rsAPI roomserverAPI.ClientRoomserverAPI,
|
||||||
fedSenderAPI federationAPI.ClientFederationAPI,
|
fedSenderAPI federationAPI.ClientFederationAPI,
|
||||||
|
@ -54,7 +55,7 @@ func DirectoryRoom(
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return util.JSONResponse{
|
return util.JSONResponse{
|
||||||
Code: http.StatusBadRequest,
|
Code: http.StatusBadRequest,
|
||||||
JSON: jsonerror.BadJSON("Room alias must be in the form '#localpart:domain'"),
|
JSON: spec.InvalidParam("Room alias must be in the form '#localpart:domain'"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -68,7 +69,10 @@ func DirectoryRoom(
|
||||||
queryRes := &roomserverAPI.GetRoomIDForAliasResponse{}
|
queryRes := &roomserverAPI.GetRoomIDForAliasResponse{}
|
||||||
if err = rsAPI.GetRoomIDForAlias(req.Context(), queryReq, queryRes); err != nil {
|
if err = rsAPI.GetRoomIDForAlias(req.Context(), queryReq, queryRes); err != nil {
|
||||||
util.GetLogger(req.Context()).WithError(err).Error("rsAPI.GetRoomIDForAlias failed")
|
util.GetLogger(req.Context()).WithError(err).Error("rsAPI.GetRoomIDForAlias failed")
|
||||||
return jsonerror.InternalServerError()
|
return util.JSONResponse{
|
||||||
|
Code: http.StatusInternalServerError,
|
||||||
|
JSON: spec.InternalServerError{},
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
res.RoomID = queryRes.RoomID
|
res.RoomID = queryRes.RoomID
|
||||||
|
@ -82,7 +86,10 @@ func DirectoryRoom(
|
||||||
// TODO: Return 502 if the remote server errored.
|
// TODO: Return 502 if the remote server errored.
|
||||||
// TODO: Return 504 if the remote server timed out.
|
// TODO: Return 504 if the remote server timed out.
|
||||||
util.GetLogger(req.Context()).WithError(fedErr).Error("federation.LookupRoomAlias failed")
|
util.GetLogger(req.Context()).WithError(fedErr).Error("federation.LookupRoomAlias failed")
|
||||||
return jsonerror.InternalServerError()
|
return util.JSONResponse{
|
||||||
|
Code: http.StatusInternalServerError,
|
||||||
|
JSON: spec.InternalServerError{},
|
||||||
|
}
|
||||||
}
|
}
|
||||||
res.RoomID = fedRes.RoomID
|
res.RoomID = fedRes.RoomID
|
||||||
res.fillServers(fedRes.Servers)
|
res.fillServers(fedRes.Servers)
|
||||||
|
@ -91,7 +98,7 @@ func DirectoryRoom(
|
||||||
if res.RoomID == "" {
|
if res.RoomID == "" {
|
||||||
return util.JSONResponse{
|
return util.JSONResponse{
|
||||||
Code: http.StatusNotFound,
|
Code: http.StatusNotFound,
|
||||||
JSON: jsonerror.NotFound(
|
JSON: spec.NotFound(
|
||||||
fmt.Sprintf("Room alias %s not found", roomAlias),
|
fmt.Sprintf("Room alias %s not found", roomAlias),
|
||||||
),
|
),
|
||||||
}
|
}
|
||||||
|
@ -101,7 +108,10 @@ func DirectoryRoom(
|
||||||
var joinedHostsRes federationAPI.QueryJoinedHostServerNamesInRoomResponse
|
var joinedHostsRes federationAPI.QueryJoinedHostServerNamesInRoomResponse
|
||||||
if err = fedSenderAPI.QueryJoinedHostServerNamesInRoom(req.Context(), &joinedHostsReq, &joinedHostsRes); err != nil {
|
if err = fedSenderAPI.QueryJoinedHostServerNamesInRoom(req.Context(), &joinedHostsReq, &joinedHostsRes); err != nil {
|
||||||
util.GetLogger(req.Context()).WithError(err).Error("fedSenderAPI.QueryJoinedHostServerNamesInRoom failed")
|
util.GetLogger(req.Context()).WithError(err).Error("fedSenderAPI.QueryJoinedHostServerNamesInRoom failed")
|
||||||
return jsonerror.InternalServerError()
|
return util.JSONResponse{
|
||||||
|
Code: http.StatusInternalServerError,
|
||||||
|
JSON: spec.InternalServerError{},
|
||||||
|
}
|
||||||
}
|
}
|
||||||
res.fillServers(joinedHostsRes.ServerNames)
|
res.fillServers(joinedHostsRes.ServerNames)
|
||||||
}
|
}
|
||||||
|
@ -124,14 +134,14 @@ func SetLocalAlias(
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return util.JSONResponse{
|
return util.JSONResponse{
|
||||||
Code: http.StatusBadRequest,
|
Code: http.StatusBadRequest,
|
||||||
JSON: jsonerror.BadJSON("Room alias must be in the form '#localpart:domain'"),
|
JSON: spec.InvalidParam("Room alias must be in the form '#localpart:domain'"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if !cfg.Matrix.IsLocalServerName(domain) {
|
if !cfg.Matrix.IsLocalServerName(domain) {
|
||||||
return util.JSONResponse{
|
return util.JSONResponse{
|
||||||
Code: http.StatusForbidden,
|
Code: http.StatusForbidden,
|
||||||
JSON: jsonerror.Forbidden("Alias must be on local homeserver"),
|
JSON: spec.Forbidden("Alias must be on local homeserver"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -144,7 +154,7 @@ func SetLocalAlias(
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return util.JSONResponse{
|
return util.JSONResponse{
|
||||||
Code: http.StatusBadRequest,
|
Code: http.StatusBadRequest,
|
||||||
JSON: jsonerror.BadJSON("User ID must be in the form '@localpart:domain'"),
|
JSON: spec.BadJSON("User ID must be in the form '@localpart:domain'"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
for _, appservice := range cfg.Derived.ApplicationServices {
|
for _, appservice := range cfg.Derived.ApplicationServices {
|
||||||
|
@ -156,7 +166,7 @@ func SetLocalAlias(
|
||||||
if namespace.Exclusive && namespace.RegexpObject.MatchString(alias) {
|
if namespace.Exclusive && namespace.RegexpObject.MatchString(alias) {
|
||||||
return util.JSONResponse{
|
return util.JSONResponse{
|
||||||
Code: http.StatusBadRequest,
|
Code: http.StatusBadRequest,
|
||||||
JSON: jsonerror.ASExclusive("Alias is reserved by an application service"),
|
JSON: spec.ASExclusive("Alias is reserved by an application service"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -171,21 +181,50 @@ func SetLocalAlias(
|
||||||
return *resErr
|
return *resErr
|
||||||
}
|
}
|
||||||
|
|
||||||
queryReq := roomserverAPI.SetRoomAliasRequest{
|
roomID, err := spec.NewRoomID(r.RoomID)
|
||||||
UserID: device.UserID,
|
if err != nil {
|
||||||
RoomID: r.RoomID,
|
return util.JSONResponse{
|
||||||
Alias: alias,
|
Code: http.StatusBadRequest,
|
||||||
}
|
JSON: spec.InvalidParam("invalid room ID"),
|
||||||
var queryRes roomserverAPI.SetRoomAliasResponse
|
}
|
||||||
if err := rsAPI.SetRoomAlias(req.Context(), &queryReq, &queryRes); err != nil {
|
|
||||||
util.GetLogger(req.Context()).WithError(err).Error("aliasAPI.SetRoomAlias failed")
|
|
||||||
return jsonerror.InternalServerError()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if queryRes.AliasExists {
|
userID, err := spec.NewUserID(device.UserID, true)
|
||||||
|
if err != nil {
|
||||||
|
return util.JSONResponse{
|
||||||
|
Code: http.StatusInternalServerError,
|
||||||
|
JSON: spec.Unknown("internal server error"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
senderID, err := rsAPI.QuerySenderIDForUser(req.Context(), *roomID, *userID)
|
||||||
|
if err != nil {
|
||||||
|
util.GetLogger(req.Context()).WithError(err).Error("QuerySenderIDForUser failed")
|
||||||
|
return util.JSONResponse{
|
||||||
|
Code: http.StatusInternalServerError,
|
||||||
|
JSON: spec.Unknown("internal server error"),
|
||||||
|
}
|
||||||
|
} else if senderID == nil {
|
||||||
|
util.GetLogger(req.Context()).WithField("roomID", *roomID).WithField("userID", *userID).Error("Sender ID not found")
|
||||||
|
return util.JSONResponse{
|
||||||
|
Code: http.StatusInternalServerError,
|
||||||
|
JSON: spec.Unknown("internal server error"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
aliasAlreadyExists, err := rsAPI.SetRoomAlias(req.Context(), *senderID, *roomID, alias)
|
||||||
|
if err != nil {
|
||||||
|
util.GetLogger(req.Context()).WithError(err).Error("aliasAPI.SetRoomAlias failed")
|
||||||
|
return util.JSONResponse{
|
||||||
|
Code: http.StatusInternalServerError,
|
||||||
|
JSON: spec.InternalServerError{},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if aliasAlreadyExists {
|
||||||
return util.JSONResponse{
|
return util.JSONResponse{
|
||||||
Code: http.StatusConflict,
|
Code: http.StatusConflict,
|
||||||
JSON: jsonerror.Unknown("The alias " + alias + " already exists."),
|
JSON: spec.Unknown("The alias " + alias + " already exists."),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -202,27 +241,91 @@ func RemoveLocalAlias(
|
||||||
alias string,
|
alias string,
|
||||||
rsAPI roomserverAPI.ClientRoomserverAPI,
|
rsAPI roomserverAPI.ClientRoomserverAPI,
|
||||||
) util.JSONResponse {
|
) util.JSONResponse {
|
||||||
queryReq := roomserverAPI.RemoveRoomAliasRequest{
|
userID, err := spec.NewUserID(device.UserID, true)
|
||||||
Alias: alias,
|
if err != nil {
|
||||||
UserID: device.UserID,
|
|
||||||
}
|
|
||||||
var queryRes roomserverAPI.RemoveRoomAliasResponse
|
|
||||||
if err := rsAPI.RemoveRoomAlias(req.Context(), &queryReq, &queryRes); err != nil {
|
|
||||||
util.GetLogger(req.Context()).WithError(err).Error("aliasAPI.RemoveRoomAlias failed")
|
|
||||||
return jsonerror.InternalServerError()
|
|
||||||
}
|
|
||||||
|
|
||||||
if !queryRes.Found {
|
|
||||||
return util.JSONResponse{
|
return util.JSONResponse{
|
||||||
Code: http.StatusNotFound,
|
Code: http.StatusInternalServerError,
|
||||||
JSON: jsonerror.NotFound("The alias does not exist."),
|
JSON: spec.InternalServerError{Err: "UserID for device is invalid"},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if !queryRes.Removed {
|
roomIDReq := roomserverAPI.GetRoomIDForAliasRequest{Alias: alias}
|
||||||
|
roomIDRes := roomserverAPI.GetRoomIDForAliasResponse{}
|
||||||
|
err = rsAPI.GetRoomIDForAlias(req.Context(), &roomIDReq, &roomIDRes)
|
||||||
|
if err != nil {
|
||||||
|
return util.JSONResponse{
|
||||||
|
Code: http.StatusNotFound,
|
||||||
|
JSON: spec.NotFound("The alias does not exist."),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
validRoomID, err := spec.NewRoomID(roomIDRes.RoomID)
|
||||||
|
if err != nil {
|
||||||
|
return util.JSONResponse{
|
||||||
|
Code: http.StatusNotFound,
|
||||||
|
JSON: spec.NotFound("The alias does not exist."),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// This seems like the kind of auth check that should be done in the roomserver, but
|
||||||
|
// if this check fails (user is not in the room), then there will be no SenderID for the user
|
||||||
|
// for pseudo-ID rooms - it will just return "". However, we can't use lack of a sender ID
|
||||||
|
// as meaning they are not in the room, since lacking a sender ID could be caused by other bugs.
|
||||||
|
// TODO: maybe have QuerySenderIDForUser return richer errors?
|
||||||
|
var queryResp roomserverAPI.QueryMembershipForUserResponse
|
||||||
|
err = rsAPI.QueryMembershipForUser(req.Context(), &roomserverAPI.QueryMembershipForUserRequest{
|
||||||
|
RoomID: validRoomID.String(),
|
||||||
|
UserID: *userID,
|
||||||
|
}, &queryResp)
|
||||||
|
if err != nil {
|
||||||
|
util.GetLogger(req.Context()).WithError(err).Error("roomserverAPI.QueryMembershipForUser failed")
|
||||||
|
return util.JSONResponse{
|
||||||
|
Code: http.StatusInternalServerError,
|
||||||
|
JSON: spec.Unknown("internal server error"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !queryResp.IsInRoom {
|
||||||
return util.JSONResponse{
|
return util.JSONResponse{
|
||||||
Code: http.StatusForbidden,
|
Code: http.StatusForbidden,
|
||||||
JSON: jsonerror.Forbidden("You do not have permission to remove this alias."),
|
JSON: spec.Forbidden("You do not have permission to remove this alias."),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
deviceSenderID, err := rsAPI.QuerySenderIDForUser(req.Context(), *validRoomID, *userID)
|
||||||
|
if err != nil {
|
||||||
|
return util.JSONResponse{
|
||||||
|
Code: http.StatusNotFound,
|
||||||
|
JSON: spec.NotFound("The alias does not exist."),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// TODO: how to handle this case? missing user/room keys seem to be a whole new class of errors
|
||||||
|
if deviceSenderID == nil {
|
||||||
|
return util.JSONResponse{
|
||||||
|
Code: http.StatusInternalServerError,
|
||||||
|
JSON: spec.Unknown("internal server error"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
aliasFound, aliasRemoved, err := rsAPI.RemoveRoomAlias(req.Context(), *deviceSenderID, alias)
|
||||||
|
if err != nil {
|
||||||
|
util.GetLogger(req.Context()).WithError(err).Error("aliasAPI.RemoveRoomAlias failed")
|
||||||
|
return util.JSONResponse{
|
||||||
|
Code: http.StatusInternalServerError,
|
||||||
|
JSON: spec.Unknown("internal server error"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !aliasFound {
|
||||||
|
return util.JSONResponse{
|
||||||
|
Code: http.StatusNotFound,
|
||||||
|
JSON: spec.NotFound("The alias does not exist."),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !aliasRemoved {
|
||||||
|
return util.JSONResponse{
|
||||||
|
Code: http.StatusForbidden,
|
||||||
|
JSON: spec.Forbidden("You do not have permission to remove this alias."),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -247,12 +350,15 @@ func GetVisibility(
|
||||||
}, &res)
|
}, &res)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
util.GetLogger(req.Context()).WithError(err).Error("QueryPublishedRooms failed")
|
util.GetLogger(req.Context()).WithError(err).Error("QueryPublishedRooms failed")
|
||||||
return jsonerror.InternalServerError()
|
return util.JSONResponse{
|
||||||
|
Code: http.StatusInternalServerError,
|
||||||
|
JSON: spec.InternalServerError{},
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var v roomVisibility
|
var v roomVisibility
|
||||||
if len(res.RoomIDs) == 1 {
|
if len(res.RoomIDs) == 1 {
|
||||||
v.Visibility = gomatrixserverlib.Public
|
v.Visibility = spec.Public
|
||||||
} else {
|
} else {
|
||||||
v.Visibility = "private"
|
v.Visibility = "private"
|
||||||
}
|
}
|
||||||
|
@ -269,7 +375,30 @@ func SetVisibility(
|
||||||
req *http.Request, rsAPI roomserverAPI.ClientRoomserverAPI, dev *userapi.Device,
|
req *http.Request, rsAPI roomserverAPI.ClientRoomserverAPI, dev *userapi.Device,
|
||||||
roomID string,
|
roomID string,
|
||||||
) util.JSONResponse {
|
) util.JSONResponse {
|
||||||
resErr := checkMemberInRoom(req.Context(), rsAPI, dev.UserID, roomID)
|
deviceUserID, err := spec.NewUserID(dev.UserID, true)
|
||||||
|
if err != nil {
|
||||||
|
return util.JSONResponse{
|
||||||
|
Code: http.StatusBadRequest,
|
||||||
|
JSON: spec.BadJSON("userID for this device is invalid"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
validRoomID, err := spec.NewRoomID(roomID)
|
||||||
|
if err != nil {
|
||||||
|
util.GetLogger(req.Context()).WithError(err).Error("roomID is invalid")
|
||||||
|
return util.JSONResponse{
|
||||||
|
Code: http.StatusBadRequest,
|
||||||
|
JSON: spec.BadJSON("RoomID is invalid"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
senderID, err := rsAPI.QuerySenderIDForUser(req.Context(), *validRoomID, *deviceUserID)
|
||||||
|
if err != nil || senderID == nil {
|
||||||
|
return util.JSONResponse{
|
||||||
|
Code: http.StatusBadRequest,
|
||||||
|
JSON: spec.Unknown("failed to find senderID for this user"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
resErr := checkMemberInRoom(req.Context(), rsAPI, *deviceUserID, roomID)
|
||||||
if resErr != nil {
|
if resErr != nil {
|
||||||
return *resErr
|
return *resErr
|
||||||
}
|
}
|
||||||
|
@ -277,23 +406,26 @@ func SetVisibility(
|
||||||
queryEventsReq := roomserverAPI.QueryLatestEventsAndStateRequest{
|
queryEventsReq := roomserverAPI.QueryLatestEventsAndStateRequest{
|
||||||
RoomID: roomID,
|
RoomID: roomID,
|
||||||
StateToFetch: []gomatrixserverlib.StateKeyTuple{{
|
StateToFetch: []gomatrixserverlib.StateKeyTuple{{
|
||||||
EventType: gomatrixserverlib.MRoomPowerLevels,
|
EventType: spec.MRoomPowerLevels,
|
||||||
StateKey: "",
|
StateKey: "",
|
||||||
}},
|
}},
|
||||||
}
|
}
|
||||||
var queryEventsRes roomserverAPI.QueryLatestEventsAndStateResponse
|
var queryEventsRes roomserverAPI.QueryLatestEventsAndStateResponse
|
||||||
err := rsAPI.QueryLatestEventsAndState(req.Context(), &queryEventsReq, &queryEventsRes)
|
err = rsAPI.QueryLatestEventsAndState(req.Context(), &queryEventsReq, &queryEventsRes)
|
||||||
if err != nil || len(queryEventsRes.StateEvents) == 0 {
|
if err != nil || len(queryEventsRes.StateEvents) == 0 {
|
||||||
util.GetLogger(req.Context()).WithError(err).Error("could not query events from room")
|
util.GetLogger(req.Context()).WithError(err).Error("could not query events from room")
|
||||||
return jsonerror.InternalServerError()
|
return util.JSONResponse{
|
||||||
|
Code: http.StatusInternalServerError,
|
||||||
|
JSON: spec.InternalServerError{},
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// NOTSPEC: Check if the user's power is greater than power required to change m.room.canonical_alias event
|
// NOTSPEC: Check if the user's power is greater than power required to change m.room.canonical_alias event
|
||||||
power, _ := gomatrixserverlib.NewPowerLevelContentFromEvent(queryEventsRes.StateEvents[0].Event)
|
power, _ := gomatrixserverlib.NewPowerLevelContentFromEvent(queryEventsRes.StateEvents[0].PDU)
|
||||||
if power.UserLevel(dev.UserID) < power.EventLevel(gomatrixserverlib.MRoomCanonicalAlias, true) {
|
if power.UserLevel(*senderID) < power.EventLevel(spec.MRoomCanonicalAlias, true) {
|
||||||
return util.JSONResponse{
|
return util.JSONResponse{
|
||||||
Code: http.StatusForbidden,
|
Code: http.StatusForbidden,
|
||||||
JSON: jsonerror.Forbidden("userID doesn't have power level to change visibility"),
|
JSON: spec.Forbidden("userID doesn't have power level to change visibility"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -302,16 +434,15 @@ func SetVisibility(
|
||||||
return *reqErr
|
return *reqErr
|
||||||
}
|
}
|
||||||
|
|
||||||
var publishRes roomserverAPI.PerformPublishResponse
|
if err = rsAPI.PerformPublish(req.Context(), &roomserverAPI.PerformPublishRequest{
|
||||||
if err := rsAPI.PerformPublish(req.Context(), &roomserverAPI.PerformPublishRequest{
|
|
||||||
RoomID: roomID,
|
RoomID: roomID,
|
||||||
Visibility: v.Visibility,
|
Visibility: v.Visibility,
|
||||||
}, &publishRes); err != nil {
|
}); err != nil {
|
||||||
return jsonerror.InternalAPIError(req.Context(), err)
|
util.GetLogger(req.Context()).WithError(err).Error("failed to publish room")
|
||||||
}
|
return util.JSONResponse{
|
||||||
if publishRes.Error != nil {
|
Code: http.StatusInternalServerError,
|
||||||
util.GetLogger(req.Context()).WithError(publishRes.Error).Error("PerformPublish failed")
|
JSON: spec.InternalServerError{},
|
||||||
return publishRes.Error.JSONResponse()
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return util.JSONResponse{
|
return util.JSONResponse{
|
||||||
|
@ -327,7 +458,7 @@ func SetVisibilityAS(
|
||||||
if dev.AccountType != userapi.AccountTypeAppService {
|
if dev.AccountType != userapi.AccountTypeAppService {
|
||||||
return util.JSONResponse{
|
return util.JSONResponse{
|
||||||
Code: http.StatusForbidden,
|
Code: http.StatusForbidden,
|
||||||
JSON: jsonerror.Forbidden("Only appservice may use this endpoint"),
|
JSON: spec.Forbidden("Only appservice may use this endpoint"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
var v roomVisibility
|
var v roomVisibility
|
||||||
|
@ -340,18 +471,17 @@ func SetVisibilityAS(
|
||||||
return *reqErr
|
return *reqErr
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
var publishRes roomserverAPI.PerformPublishResponse
|
|
||||||
if err := rsAPI.PerformPublish(req.Context(), &roomserverAPI.PerformPublishRequest{
|
if err := rsAPI.PerformPublish(req.Context(), &roomserverAPI.PerformPublishRequest{
|
||||||
RoomID: roomID,
|
RoomID: roomID,
|
||||||
Visibility: v.Visibility,
|
Visibility: v.Visibility,
|
||||||
NetworkID: networkID,
|
NetworkID: networkID,
|
||||||
AppserviceID: dev.AppserviceID,
|
AppserviceID: dev.AppserviceID,
|
||||||
}, &publishRes); err != nil {
|
}); err != nil {
|
||||||
return jsonerror.InternalAPIError(req.Context(), err)
|
util.GetLogger(req.Context()).WithError(err).Error("failed to publish room")
|
||||||
}
|
return util.JSONResponse{
|
||||||
if publishRes.Error != nil {
|
Code: http.StatusInternalServerError,
|
||||||
util.GetLogger(req.Context()).WithError(publishRes.Error).Error("PerformPublish failed")
|
JSON: spec.InternalServerError{},
|
||||||
return publishRes.Error.JSONResponse()
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return util.JSONResponse{
|
return util.JSONResponse{
|
||||||
|
|
|
@ -23,19 +23,19 @@ import (
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
|
|
||||||
"github.com/matrix-org/gomatrixserverlib"
|
"github.com/matrix-org/gomatrixserverlib/fclient"
|
||||||
|
"github.com/matrix-org/gomatrixserverlib/spec"
|
||||||
"github.com/matrix-org/util"
|
"github.com/matrix-org/util"
|
||||||
|
|
||||||
"github.com/matrix-org/dendrite/clientapi/api"
|
"github.com/matrix-org/dendrite/clientapi/api"
|
||||||
"github.com/matrix-org/dendrite/clientapi/httputil"
|
"github.com/matrix-org/dendrite/clientapi/httputil"
|
||||||
"github.com/matrix-org/dendrite/clientapi/jsonerror"
|
|
||||||
roomserverAPI "github.com/matrix-org/dendrite/roomserver/api"
|
roomserverAPI "github.com/matrix-org/dendrite/roomserver/api"
|
||||||
"github.com/matrix-org/dendrite/setup/config"
|
"github.com/matrix-org/dendrite/setup/config"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
cacheMu sync.Mutex
|
cacheMu sync.Mutex
|
||||||
publicRoomsCache []gomatrixserverlib.PublicRoom
|
publicRoomsCache []fclient.PublicRoom
|
||||||
)
|
)
|
||||||
|
|
||||||
type PublicRoomReq struct {
|
type PublicRoomReq struct {
|
||||||
|
@ -56,7 +56,7 @@ type filter struct {
|
||||||
func GetPostPublicRooms(
|
func GetPostPublicRooms(
|
||||||
req *http.Request, rsAPI roomserverAPI.ClientRoomserverAPI,
|
req *http.Request, rsAPI roomserverAPI.ClientRoomserverAPI,
|
||||||
extRoomsProvider api.ExtraPublicRoomsProvider,
|
extRoomsProvider api.ExtraPublicRoomsProvider,
|
||||||
federation *gomatrixserverlib.FederationClient,
|
federation fclient.FederationClient,
|
||||||
cfg *config.ClientAPI,
|
cfg *config.ClientAPI,
|
||||||
) util.JSONResponse {
|
) util.JSONResponse {
|
||||||
var request PublicRoomReq
|
var request PublicRoomReq
|
||||||
|
@ -67,11 +67,11 @@ func GetPostPublicRooms(
|
||||||
if request.IncludeAllNetworks && request.NetworkID != "" {
|
if request.IncludeAllNetworks && request.NetworkID != "" {
|
||||||
return util.JSONResponse{
|
return util.JSONResponse{
|
||||||
Code: http.StatusBadRequest,
|
Code: http.StatusBadRequest,
|
||||||
JSON: jsonerror.InvalidParam("include_all_networks and third_party_instance_id can not be used together"),
|
JSON: spec.InvalidParam("include_all_networks and third_party_instance_id can not be used together"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
serverName := gomatrixserverlib.ServerName(request.Server)
|
serverName := spec.ServerName(request.Server)
|
||||||
if serverName != "" && !cfg.Matrix.IsLocalServerName(serverName) {
|
if serverName != "" && !cfg.Matrix.IsLocalServerName(serverName) {
|
||||||
res, err := federation.GetPublicRoomsFiltered(
|
res, err := federation.GetPublicRoomsFiltered(
|
||||||
req.Context(), cfg.Matrix.ServerName, serverName,
|
req.Context(), cfg.Matrix.ServerName, serverName,
|
||||||
|
@ -81,7 +81,10 @@ func GetPostPublicRooms(
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
util.GetLogger(req.Context()).WithError(err).Error("failed to get public rooms")
|
util.GetLogger(req.Context()).WithError(err).Error("failed to get public rooms")
|
||||||
return jsonerror.InternalServerError()
|
return util.JSONResponse{
|
||||||
|
Code: http.StatusInternalServerError,
|
||||||
|
JSON: spec.InternalServerError{},
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return util.JSONResponse{
|
return util.JSONResponse{
|
||||||
Code: http.StatusOK,
|
Code: http.StatusOK,
|
||||||
|
@ -92,7 +95,10 @@ func GetPostPublicRooms(
|
||||||
response, err := publicRooms(req.Context(), request, rsAPI, extRoomsProvider)
|
response, err := publicRooms(req.Context(), request, rsAPI, extRoomsProvider)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
util.GetLogger(req.Context()).WithError(err).Errorf("failed to work out public rooms")
|
util.GetLogger(req.Context()).WithError(err).Errorf("failed to work out public rooms")
|
||||||
return jsonerror.InternalServerError()
|
return util.JSONResponse{
|
||||||
|
Code: http.StatusInternalServerError,
|
||||||
|
JSON: spec.InternalServerError{},
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return util.JSONResponse{
|
return util.JSONResponse{
|
||||||
Code: http.StatusOK,
|
Code: http.StatusOK,
|
||||||
|
@ -102,10 +108,10 @@ func GetPostPublicRooms(
|
||||||
|
|
||||||
func publicRooms(
|
func publicRooms(
|
||||||
ctx context.Context, request PublicRoomReq, rsAPI roomserverAPI.ClientRoomserverAPI, extRoomsProvider api.ExtraPublicRoomsProvider,
|
ctx context.Context, request PublicRoomReq, rsAPI roomserverAPI.ClientRoomserverAPI, extRoomsProvider api.ExtraPublicRoomsProvider,
|
||||||
) (*gomatrixserverlib.RespPublicRooms, error) {
|
) (*fclient.RespPublicRooms, error) {
|
||||||
|
|
||||||
response := gomatrixserverlib.RespPublicRooms{
|
response := fclient.RespPublicRooms{
|
||||||
Chunk: []gomatrixserverlib.PublicRoom{},
|
Chunk: []fclient.PublicRoom{},
|
||||||
}
|
}
|
||||||
var limit int64
|
var limit int64
|
||||||
var offset int64
|
var offset int64
|
||||||
|
@ -122,7 +128,7 @@ func publicRooms(
|
||||||
}
|
}
|
||||||
err = nil
|
err = nil
|
||||||
|
|
||||||
var rooms []gomatrixserverlib.PublicRoom
|
var rooms []fclient.PublicRoom
|
||||||
if request.Since == "" {
|
if request.Since == "" {
|
||||||
rooms = refreshPublicRoomCache(ctx, rsAPI, extRoomsProvider, request)
|
rooms = refreshPublicRoomCache(ctx, rsAPI, extRoomsProvider, request)
|
||||||
} else {
|
} else {
|
||||||
|
@ -146,14 +152,14 @@ func publicRooms(
|
||||||
return &response, err
|
return &response, err
|
||||||
}
|
}
|
||||||
|
|
||||||
func filterRooms(rooms []gomatrixserverlib.PublicRoom, searchTerm string) []gomatrixserverlib.PublicRoom {
|
func filterRooms(rooms []fclient.PublicRoom, searchTerm string) []fclient.PublicRoom {
|
||||||
if searchTerm == "" {
|
if searchTerm == "" {
|
||||||
return rooms
|
return rooms
|
||||||
}
|
}
|
||||||
|
|
||||||
normalizedTerm := strings.ToLower(searchTerm)
|
normalizedTerm := strings.ToLower(searchTerm)
|
||||||
|
|
||||||
result := make([]gomatrixserverlib.PublicRoom, 0)
|
result := make([]fclient.PublicRoom, 0)
|
||||||
for _, room := range rooms {
|
for _, room := range rooms {
|
||||||
if strings.Contains(strings.ToLower(room.Name), normalizedTerm) ||
|
if strings.Contains(strings.ToLower(room.Name), normalizedTerm) ||
|
||||||
strings.Contains(strings.ToLower(room.Topic), normalizedTerm) ||
|
strings.Contains(strings.ToLower(room.Topic), normalizedTerm) ||
|
||||||
|
@ -172,7 +178,7 @@ func fillPublicRoomsReq(httpReq *http.Request, request *PublicRoomReq) *util.JSO
|
||||||
if httpReq.Method != "GET" && httpReq.Method != "POST" {
|
if httpReq.Method != "GET" && httpReq.Method != "POST" {
|
||||||
return &util.JSONResponse{
|
return &util.JSONResponse{
|
||||||
Code: http.StatusMethodNotAllowed,
|
Code: http.StatusMethodNotAllowed,
|
||||||
JSON: jsonerror.NotFound("Bad method"),
|
JSON: spec.NotFound("Bad method"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if httpReq.Method == "GET" {
|
if httpReq.Method == "GET" {
|
||||||
|
@ -183,7 +189,7 @@ func fillPublicRoomsReq(httpReq *http.Request, request *PublicRoomReq) *util.JSO
|
||||||
util.GetLogger(httpReq.Context()).WithError(err).Error("strconv.Atoi failed")
|
util.GetLogger(httpReq.Context()).WithError(err).Error("strconv.Atoi failed")
|
||||||
return &util.JSONResponse{
|
return &util.JSONResponse{
|
||||||
Code: 400,
|
Code: 400,
|
||||||
JSON: jsonerror.BadJSON("limit param is not a number"),
|
JSON: spec.BadJSON("limit param is not a number"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
request.Limit = int64(limit)
|
request.Limit = int64(limit)
|
||||||
|
@ -214,7 +220,7 @@ func fillPublicRoomsReq(httpReq *http.Request, request *PublicRoomReq) *util.JSO
|
||||||
// limit=3&since=6 => G (prev='3', next='')
|
// limit=3&since=6 => G (prev='3', next='')
|
||||||
//
|
//
|
||||||
// A value of '-1' for prev/next indicates no position.
|
// A value of '-1' for prev/next indicates no position.
|
||||||
func sliceInto(slice []gomatrixserverlib.PublicRoom, since int64, limit int64) (subset []gomatrixserverlib.PublicRoom, prev, next int) {
|
func sliceInto(slice []fclient.PublicRoom, since int64, limit int64) (subset []fclient.PublicRoom, prev, next int) {
|
||||||
prev = -1
|
prev = -1
|
||||||
next = -1
|
next = -1
|
||||||
|
|
||||||
|
@ -241,10 +247,10 @@ func sliceInto(slice []gomatrixserverlib.PublicRoom, since int64, limit int64) (
|
||||||
func refreshPublicRoomCache(
|
func refreshPublicRoomCache(
|
||||||
ctx context.Context, rsAPI roomserverAPI.ClientRoomserverAPI, extRoomsProvider api.ExtraPublicRoomsProvider,
|
ctx context.Context, rsAPI roomserverAPI.ClientRoomserverAPI, extRoomsProvider api.ExtraPublicRoomsProvider,
|
||||||
request PublicRoomReq,
|
request PublicRoomReq,
|
||||||
) []gomatrixserverlib.PublicRoom {
|
) []fclient.PublicRoom {
|
||||||
cacheMu.Lock()
|
cacheMu.Lock()
|
||||||
defer cacheMu.Unlock()
|
defer cacheMu.Unlock()
|
||||||
var extraRooms []gomatrixserverlib.PublicRoom
|
var extraRooms []fclient.PublicRoom
|
||||||
if extRoomsProvider != nil {
|
if extRoomsProvider != nil {
|
||||||
extraRooms = extRoomsProvider.Rooms()
|
extraRooms = extRoomsProvider.Rooms()
|
||||||
}
|
}
|
||||||
|
@ -269,7 +275,7 @@ func refreshPublicRoomCache(
|
||||||
util.GetLogger(ctx).WithError(err).Error("PopulatePublicRooms failed")
|
util.GetLogger(ctx).WithError(err).Error("PopulatePublicRooms failed")
|
||||||
return publicRoomsCache
|
return publicRoomsCache
|
||||||
}
|
}
|
||||||
publicRoomsCache = []gomatrixserverlib.PublicRoom{}
|
publicRoomsCache = []fclient.PublicRoom{}
|
||||||
publicRoomsCache = append(publicRoomsCache, pubRooms...)
|
publicRoomsCache = append(publicRoomsCache, pubRooms...)
|
||||||
publicRoomsCache = append(publicRoomsCache, extraRooms...)
|
publicRoomsCache = append(publicRoomsCache, extraRooms...)
|
||||||
publicRoomsCache = dedupeAndShuffle(publicRoomsCache)
|
publicRoomsCache = dedupeAndShuffle(publicRoomsCache)
|
||||||
|
@ -281,16 +287,16 @@ func refreshPublicRoomCache(
|
||||||
return publicRoomsCache
|
return publicRoomsCache
|
||||||
}
|
}
|
||||||
|
|
||||||
func getPublicRoomsFromCache() []gomatrixserverlib.PublicRoom {
|
func getPublicRoomsFromCache() []fclient.PublicRoom {
|
||||||
cacheMu.Lock()
|
cacheMu.Lock()
|
||||||
defer cacheMu.Unlock()
|
defer cacheMu.Unlock()
|
||||||
return publicRoomsCache
|
return publicRoomsCache
|
||||||
}
|
}
|
||||||
|
|
||||||
func dedupeAndShuffle(in []gomatrixserverlib.PublicRoom) []gomatrixserverlib.PublicRoom {
|
func dedupeAndShuffle(in []fclient.PublicRoom) []fclient.PublicRoom {
|
||||||
// de-duplicate rooms with the same room ID. We can join the room via any of these aliases as we know these servers
|
// de-duplicate rooms with the same room ID. We can join the room via any of these aliases as we know these servers
|
||||||
// are alive and well, so we arbitrarily pick one (purposefully shuffling them to spread the load a bit)
|
// are alive and well, so we arbitrarily pick one (purposefully shuffling them to spread the load a bit)
|
||||||
var publicRooms []gomatrixserverlib.PublicRoom
|
var publicRooms []fclient.PublicRoom
|
||||||
haveRoomIDs := make(map[string]bool)
|
haveRoomIDs := make(map[string]bool)
|
||||||
rand.Shuffle(len(in), func(i, j int) {
|
rand.Shuffle(len(in), func(i, j int) {
|
||||||
in[i], in[j] = in[j], in[i]
|
in[i], in[j] = in[j], in[i]
|
||||||
|
|
|
@ -4,17 +4,17 @@ import (
|
||||||
"reflect"
|
"reflect"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/matrix-org/gomatrixserverlib"
|
"github.com/matrix-org/gomatrixserverlib/fclient"
|
||||||
)
|
)
|
||||||
|
|
||||||
func pubRoom(name string) gomatrixserverlib.PublicRoom {
|
func pubRoom(name string) fclient.PublicRoom {
|
||||||
return gomatrixserverlib.PublicRoom{
|
return fclient.PublicRoom{
|
||||||
Name: name,
|
Name: name,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestSliceInto(t *testing.T) {
|
func TestSliceInto(t *testing.T) {
|
||||||
slice := []gomatrixserverlib.PublicRoom{
|
slice := []fclient.PublicRoom{
|
||||||
pubRoom("a"), pubRoom("b"), pubRoom("c"), pubRoom("d"), pubRoom("e"), pubRoom("f"), pubRoom("g"),
|
pubRoom("a"), pubRoom("b"), pubRoom("c"), pubRoom("d"), pubRoom("e"), pubRoom("f"), pubRoom("g"),
|
||||||
}
|
}
|
||||||
limit := int64(3)
|
limit := int64(3)
|
||||||
|
@ -22,7 +22,7 @@ func TestSliceInto(t *testing.T) {
|
||||||
since int64
|
since int64
|
||||||
wantPrev int
|
wantPrev int
|
||||||
wantNext int
|
wantNext int
|
||||||
wantSubset []gomatrixserverlib.PublicRoom
|
wantSubset []fclient.PublicRoom
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
since: 0,
|
since: 0,
|
||||||
|
|
|
@ -19,9 +19,9 @@ import (
|
||||||
|
|
||||||
"github.com/matrix-org/util"
|
"github.com/matrix-org/util"
|
||||||
|
|
||||||
"github.com/matrix-org/dendrite/clientapi/jsonerror"
|
|
||||||
"github.com/matrix-org/dendrite/roomserver/api"
|
"github.com/matrix-org/dendrite/roomserver/api"
|
||||||
userapi "github.com/matrix-org/dendrite/userapi/api"
|
userapi "github.com/matrix-org/dendrite/userapi/api"
|
||||||
|
"github.com/matrix-org/gomatrixserverlib/spec"
|
||||||
)
|
)
|
||||||
|
|
||||||
type getJoinedRoomsResponse struct {
|
type getJoinedRoomsResponse struct {
|
||||||
|
@ -33,20 +33,36 @@ func GetJoinedRooms(
|
||||||
device *userapi.Device,
|
device *userapi.Device,
|
||||||
rsAPI api.ClientRoomserverAPI,
|
rsAPI api.ClientRoomserverAPI,
|
||||||
) util.JSONResponse {
|
) util.JSONResponse {
|
||||||
var res api.QueryRoomsForUserResponse
|
deviceUserID, err := spec.NewUserID(device.UserID, true)
|
||||||
err := rsAPI.QueryRoomsForUser(req.Context(), &api.QueryRoomsForUserRequest{
|
if err != nil {
|
||||||
UserID: device.UserID,
|
util.GetLogger(req.Context()).WithError(err).Error("Invalid device user ID")
|
||||||
WantMembership: "join",
|
return util.JSONResponse{
|
||||||
}, &res)
|
Code: http.StatusInternalServerError,
|
||||||
|
JSON: spec.Unknown("internal server error"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
rooms, err := rsAPI.QueryRoomsForUser(req.Context(), *deviceUserID, "join")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
util.GetLogger(req.Context()).WithError(err).Error("QueryRoomsForUser failed")
|
util.GetLogger(req.Context()).WithError(err).Error("QueryRoomsForUser failed")
|
||||||
return jsonerror.InternalServerError()
|
return util.JSONResponse{
|
||||||
|
Code: http.StatusInternalServerError,
|
||||||
|
JSON: spec.Unknown("internal server error"),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if res.RoomIDs == nil {
|
|
||||||
res.RoomIDs = []string{}
|
var roomIDStrs []string
|
||||||
|
if rooms == nil {
|
||||||
|
roomIDStrs = []string{}
|
||||||
|
} else {
|
||||||
|
roomIDStrs = make([]string, len(rooms))
|
||||||
|
for i, roomID := range rooms {
|
||||||
|
roomIDStrs[i] = roomID.String()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return util.JSONResponse{
|
return util.JSONResponse{
|
||||||
Code: http.StatusOK,
|
Code: http.StatusOK,
|
||||||
JSON: getJoinedRoomsResponse{res.RoomIDs},
|
JSON: getJoinedRoomsResponse{roomIDStrs},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -15,14 +15,17 @@
|
||||||
package routing
|
package routing
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"encoding/json"
|
||||||
"net/http"
|
"net/http"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
appserviceAPI "github.com/matrix-org/dendrite/appservice/api"
|
||||||
"github.com/matrix-org/dendrite/clientapi/httputil"
|
"github.com/matrix-org/dendrite/clientapi/httputil"
|
||||||
"github.com/matrix-org/dendrite/clientapi/jsonerror"
|
"github.com/matrix-org/dendrite/internal/eventutil"
|
||||||
roomserverAPI "github.com/matrix-org/dendrite/roomserver/api"
|
roomserverAPI "github.com/matrix-org/dendrite/roomserver/api"
|
||||||
"github.com/matrix-org/dendrite/userapi/api"
|
"github.com/matrix-org/dendrite/userapi/api"
|
||||||
"github.com/matrix-org/gomatrixserverlib"
|
"github.com/matrix-org/gomatrix"
|
||||||
|
"github.com/matrix-org/gomatrixserverlib/spec"
|
||||||
"github.com/matrix-org/util"
|
"github.com/matrix-org/util"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -40,7 +43,6 @@ func JoinRoomByIDOrAlias(
|
||||||
IsGuest: device.AccountType == api.AccountTypeGuest,
|
IsGuest: device.AccountType == api.AccountTypeGuest,
|
||||||
Content: map[string]interface{}{},
|
Content: map[string]interface{}{},
|
||||||
}
|
}
|
||||||
joinRes := roomserverAPI.PerformJoinResponse{}
|
|
||||||
|
|
||||||
// Check to see if any ?server_name= query parameters were
|
// Check to see if any ?server_name= query parameters were
|
||||||
// given in the request.
|
// given in the request.
|
||||||
|
@ -48,7 +50,7 @@ func JoinRoomByIDOrAlias(
|
||||||
for _, serverName := range serverNames {
|
for _, serverName := range serverNames {
|
||||||
joinReq.ServerNames = append(
|
joinReq.ServerNames = append(
|
||||||
joinReq.ServerNames,
|
joinReq.ServerNames,
|
||||||
gomatrixserverlib.ServerName(serverName),
|
spec.ServerName(serverName),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -61,58 +63,84 @@ func JoinRoomByIDOrAlias(
|
||||||
// Work out our localpart for the client profile request.
|
// Work out our localpart for the client profile request.
|
||||||
|
|
||||||
// Request our profile content to populate the request content with.
|
// Request our profile content to populate the request content with.
|
||||||
res := &api.QueryProfileResponse{}
|
profile, err := profileAPI.QueryProfile(req.Context(), device.UserID)
|
||||||
err := profileAPI.QueryProfile(req.Context(), &api.QueryProfileRequest{UserID: device.UserID}, res)
|
|
||||||
if err != nil || !res.UserExists {
|
|
||||||
if !res.UserExists {
|
|
||||||
util.GetLogger(req.Context()).Error("Unable to query user profile, no profile found.")
|
|
||||||
return util.JSONResponse{
|
|
||||||
Code: http.StatusInternalServerError,
|
|
||||||
JSON: jsonerror.Unknown("Unable to query user profile, no profile found."),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
util.GetLogger(req.Context()).WithError(err).Error("UserProfileAPI.QueryProfile failed")
|
switch err {
|
||||||
} else {
|
case nil:
|
||||||
joinReq.Content["displayname"] = res.DisplayName
|
joinReq.Content["displayname"] = profile.DisplayName
|
||||||
joinReq.Content["avatar_url"] = res.AvatarURL
|
joinReq.Content["avatar_url"] = profile.AvatarURL
|
||||||
|
case appserviceAPI.ErrProfileNotExists:
|
||||||
|
util.GetLogger(req.Context()).Error("Unable to query user profile, no profile found.")
|
||||||
|
return util.JSONResponse{
|
||||||
|
Code: http.StatusInternalServerError,
|
||||||
|
JSON: spec.Unknown("Unable to query user profile, no profile found."),
|
||||||
|
}
|
||||||
|
default:
|
||||||
}
|
}
|
||||||
|
|
||||||
// Ask the roomserver to perform the join.
|
// Ask the roomserver to perform the join.
|
||||||
done := make(chan util.JSONResponse, 1)
|
done := make(chan util.JSONResponse, 1)
|
||||||
go func() {
|
go func() {
|
||||||
defer close(done)
|
defer close(done)
|
||||||
if err := rsAPI.PerformJoin(req.Context(), &joinReq, &joinRes); err != nil {
|
roomID, _, err := rsAPI.PerformJoin(req.Context(), &joinReq)
|
||||||
done <- jsonerror.InternalAPIError(req.Context(), err)
|
var response util.JSONResponse
|
||||||
} else if joinRes.Error != nil {
|
|
||||||
if joinRes.Error.Code == roomserverAPI.PerformErrorNotAllowed && device.AccountType == api.AccountTypeGuest {
|
switch e := err.(type) {
|
||||||
done <- util.JSONResponse{
|
case nil: // success case
|
||||||
Code: http.StatusForbidden,
|
response = util.JSONResponse{
|
||||||
JSON: jsonerror.GuestAccessForbidden(joinRes.Error.Msg),
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
done <- joinRes.Error.JSONResponse()
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
done <- util.JSONResponse{
|
|
||||||
Code: http.StatusOK,
|
Code: http.StatusOK,
|
||||||
// TODO: Put the response struct somewhere internal.
|
// TODO: Put the response struct somewhere internal.
|
||||||
JSON: struct {
|
JSON: struct {
|
||||||
RoomID string `json:"room_id"`
|
RoomID string `json:"room_id"`
|
||||||
}{joinRes.RoomID},
|
}{roomID},
|
||||||
|
}
|
||||||
|
case roomserverAPI.ErrInvalidID:
|
||||||
|
response = util.JSONResponse{
|
||||||
|
Code: http.StatusBadRequest,
|
||||||
|
JSON: spec.Unknown(e.Error()),
|
||||||
|
}
|
||||||
|
case roomserverAPI.ErrNotAllowed:
|
||||||
|
jsonErr := spec.Forbidden(e.Error())
|
||||||
|
if device.AccountType == api.AccountTypeGuest {
|
||||||
|
jsonErr = spec.GuestAccessForbidden(e.Error())
|
||||||
|
}
|
||||||
|
response = util.JSONResponse{
|
||||||
|
Code: http.StatusForbidden,
|
||||||
|
JSON: jsonErr,
|
||||||
|
}
|
||||||
|
case *gomatrix.HTTPError: // this ensures we proxy responses over federation to the client
|
||||||
|
response = util.JSONResponse{
|
||||||
|
Code: e.Code,
|
||||||
|
JSON: json.RawMessage(e.Message),
|
||||||
|
}
|
||||||
|
case eventutil.ErrRoomNoExists:
|
||||||
|
response = util.JSONResponse{
|
||||||
|
Code: http.StatusNotFound,
|
||||||
|
JSON: spec.NotFound(e.Error()),
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
response = util.JSONResponse{
|
||||||
|
Code: http.StatusInternalServerError,
|
||||||
|
JSON: spec.InternalServerError{},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
done <- response
|
||||||
}()
|
}()
|
||||||
|
|
||||||
// Wait either for the join to finish, or for us to hit a reasonable
|
// Wait either for the join to finish, or for us to hit a reasonable
|
||||||
// timeout, at which point we'll just return a 200 to placate clients.
|
// timeout, at which point we'll just return a 200 to placate clients.
|
||||||
|
timer := time.NewTimer(time.Second * 20)
|
||||||
select {
|
select {
|
||||||
case <-time.After(time.Second * 20):
|
case <-timer.C:
|
||||||
return util.JSONResponse{
|
return util.JSONResponse{
|
||||||
Code: http.StatusAccepted,
|
Code: http.StatusAccepted,
|
||||||
JSON: jsonerror.Unknown("The room join will continue in the background."),
|
JSON: spec.Unknown("The room join will continue in the background."),
|
||||||
}
|
}
|
||||||
case result := <-done:
|
case result := <-done:
|
||||||
|
// Stop and drain the timer
|
||||||
|
if !timer.Stop() {
|
||||||
|
<-timer.C
|
||||||
|
}
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,7 +7,12 @@ import (
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/matrix-org/dendrite/federationapi/statistics"
|
||||||
|
"github.com/matrix-org/dendrite/internal/caching"
|
||||||
|
"github.com/matrix-org/dendrite/internal/sqlutil"
|
||||||
|
"github.com/matrix-org/dendrite/setup/jetstream"
|
||||||
"github.com/matrix-org/gomatrixserverlib"
|
"github.com/matrix-org/gomatrixserverlib"
|
||||||
|
"github.com/matrix-org/gomatrixserverlib/spec"
|
||||||
|
|
||||||
"github.com/matrix-org/dendrite/appservice"
|
"github.com/matrix-org/dendrite/appservice"
|
||||||
"github.com/matrix-org/dendrite/roomserver"
|
"github.com/matrix-org/dendrite/roomserver"
|
||||||
|
@ -17,6 +22,10 @@ import (
|
||||||
uapi "github.com/matrix-org/dendrite/userapi/api"
|
uapi "github.com/matrix-org/dendrite/userapi/api"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
var testIsBlacklistedOrBackingOff = func(s spec.ServerName) (*statistics.ServerStatistics, error) {
|
||||||
|
return &statistics.ServerStatistics{}, nil
|
||||||
|
}
|
||||||
|
|
||||||
func TestJoinRoomByIDOrAlias(t *testing.T) {
|
func TestJoinRoomByIDOrAlias(t *testing.T) {
|
||||||
alice := test.NewUser(t)
|
alice := test.NewUser(t)
|
||||||
bob := test.NewUser(t)
|
bob := test.NewUser(t)
|
||||||
|
@ -24,13 +33,16 @@ func TestJoinRoomByIDOrAlias(t *testing.T) {
|
||||||
|
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
test.WithAllDatabases(t, func(t *testing.T, dbType test.DBType) {
|
test.WithAllDatabases(t, func(t *testing.T, dbType test.DBType) {
|
||||||
base, baseClose := testrig.CreateBaseDendrite(t, dbType)
|
cfg, processCtx, close := testrig.CreateConfig(t, dbType)
|
||||||
defer baseClose()
|
defer close()
|
||||||
|
|
||||||
rsAPI := roomserver.NewInternalAPI(base)
|
cm := sqlutil.NewConnectionManager(processCtx, cfg.Global.DatabaseOptions)
|
||||||
userAPI := userapi.NewInternalAPI(base, rsAPI, nil)
|
caches := caching.NewRistrettoCache(128*1024*1024, time.Hour, caching.DisableMetrics)
|
||||||
asAPI := appservice.NewInternalAPI(base, userAPI, rsAPI)
|
natsInstance := jetstream.NATSInstance{}
|
||||||
|
rsAPI := roomserver.NewInternalAPI(processCtx, cfg, cm, &natsInstance, caches, caching.DisableMetrics)
|
||||||
rsAPI.SetFederationAPI(nil, nil) // creates the rs.Inputer etc
|
rsAPI.SetFederationAPI(nil, nil) // creates the rs.Inputer etc
|
||||||
|
userAPI := userapi.NewInternalAPI(processCtx, cfg, cm, &natsInstance, rsAPI, nil, caching.DisableMetrics, testIsBlacklistedOrBackingOff)
|
||||||
|
asAPI := appservice.NewInternalAPI(processCtx, cfg, &natsInstance, userAPI, rsAPI)
|
||||||
|
|
||||||
// Create the users in the userapi
|
// Create the users in the userapi
|
||||||
for _, u := range []*test.User{alice, bob, charlie} {
|
for _, u := range []*test.User{alice, bob, charlie} {
|
||||||
|
@ -57,11 +69,10 @@ func TestJoinRoomByIDOrAlias(t *testing.T) {
|
||||||
IsDirect: true,
|
IsDirect: true,
|
||||||
Topic: "testing",
|
Topic: "testing",
|
||||||
Visibility: "public",
|
Visibility: "public",
|
||||||
Preset: presetPublicChat,
|
Preset: spec.PresetPublicChat,
|
||||||
RoomAliasName: "alias",
|
RoomAliasName: "alias",
|
||||||
Invite: []string{bob.ID},
|
Invite: []string{bob.ID},
|
||||||
GuestCanJoin: false,
|
}, aliceDev, &cfg.ClientAPI, userAPI, rsAPI, asAPI, time.Now())
|
||||||
}, aliceDev, &base.Cfg.ClientAPI, userAPI, rsAPI, asAPI, time.Now())
|
|
||||||
crResp, ok := resp.JSON.(createRoomResponse)
|
crResp, ok := resp.JSON.(createRoomResponse)
|
||||||
if !ok {
|
if !ok {
|
||||||
t.Fatalf("response is not a createRoomResponse: %+v", resp)
|
t.Fatalf("response is not a createRoomResponse: %+v", resp)
|
||||||
|
@ -69,14 +80,13 @@ func TestJoinRoomByIDOrAlias(t *testing.T) {
|
||||||
|
|
||||||
// create a room with guest access enabled and invite Charlie
|
// create a room with guest access enabled and invite Charlie
|
||||||
resp = createRoom(ctx, createRoomRequest{
|
resp = createRoom(ctx, createRoomRequest{
|
||||||
Name: "testing",
|
Name: "testing",
|
||||||
IsDirect: true,
|
IsDirect: true,
|
||||||
Topic: "testing",
|
Topic: "testing",
|
||||||
Visibility: "public",
|
Visibility: "public",
|
||||||
Preset: presetPublicChat,
|
Preset: spec.PresetPublicChat,
|
||||||
Invite: []string{charlie.ID},
|
Invite: []string{charlie.ID},
|
||||||
GuestCanJoin: true,
|
}, aliceDev, &cfg.ClientAPI, userAPI, rsAPI, asAPI, time.Now())
|
||||||
}, aliceDev, &base.Cfg.ClientAPI, userAPI, rsAPI, asAPI, time.Now())
|
|
||||||
crRespWithGuestAccess, ok := resp.JSON.(createRoomResponse)
|
crRespWithGuestAccess, ok := resp.JSON.(createRoomResponse)
|
||||||
if !ok {
|
if !ok {
|
||||||
t.Fatalf("response is not a createRoomResponse: %+v", resp)
|
t.Fatalf("response is not a createRoomResponse: %+v", resp)
|
||||||
|
|
|
@ -20,8 +20,8 @@ import (
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
"github.com/matrix-org/dendrite/clientapi/httputil"
|
"github.com/matrix-org/dendrite/clientapi/httputil"
|
||||||
"github.com/matrix-org/dendrite/clientapi/jsonerror"
|
|
||||||
userapi "github.com/matrix-org/dendrite/userapi/api"
|
userapi "github.com/matrix-org/dendrite/userapi/api"
|
||||||
|
"github.com/matrix-org/gomatrixserverlib/spec"
|
||||||
"github.com/matrix-org/util"
|
"github.com/matrix-org/util"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -61,28 +61,26 @@ func CreateKeyBackupVersion(req *http.Request, userAPI userapi.ClientUserAPI, de
|
||||||
if resErr != nil {
|
if resErr != nil {
|
||||||
return *resErr
|
return *resErr
|
||||||
}
|
}
|
||||||
var performKeyBackupResp userapi.PerformKeyBackupResponse
|
if len(kb.AuthData) == 0 {
|
||||||
if err := userAPI.PerformKeyBackup(req.Context(), &userapi.PerformKeyBackupRequest{
|
return util.JSONResponse{
|
||||||
|
Code: http.StatusBadRequest,
|
||||||
|
JSON: spec.BadJSON("missing auth_data"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
version, err := userAPI.PerformKeyBackup(req.Context(), &userapi.PerformKeyBackupRequest{
|
||||||
UserID: device.UserID,
|
UserID: device.UserID,
|
||||||
Version: "",
|
Version: "",
|
||||||
AuthData: kb.AuthData,
|
AuthData: kb.AuthData,
|
||||||
Algorithm: kb.Algorithm,
|
Algorithm: kb.Algorithm,
|
||||||
}, &performKeyBackupResp); err != nil {
|
})
|
||||||
return jsonerror.InternalServerError()
|
if err != nil {
|
||||||
}
|
return util.ErrorResponse(fmt.Errorf("PerformKeyBackup: %w", err))
|
||||||
if performKeyBackupResp.Error != "" {
|
|
||||||
if performKeyBackupResp.BadInput {
|
|
||||||
return util.JSONResponse{
|
|
||||||
Code: 400,
|
|
||||||
JSON: jsonerror.InvalidArgumentValue(performKeyBackupResp.Error),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return util.ErrorResponse(fmt.Errorf("PerformKeyBackup: %s", performKeyBackupResp.Error))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return util.JSONResponse{
|
return util.JSONResponse{
|
||||||
Code: 200,
|
Code: 200,
|
||||||
JSON: keyBackupVersionCreateResponse{
|
JSON: keyBackupVersionCreateResponse{
|
||||||
Version: performKeyBackupResp.Version,
|
Version: version,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -90,20 +88,17 @@ func CreateKeyBackupVersion(req *http.Request, userAPI userapi.ClientUserAPI, de
|
||||||
// KeyBackupVersion returns the key backup version specified. If `version` is empty, the latest `keyBackupVersionResponse` is returned.
|
// KeyBackupVersion returns the key backup version specified. If `version` is empty, the latest `keyBackupVersionResponse` is returned.
|
||||||
// Implements GET /_matrix/client/r0/room_keys/version and GET /_matrix/client/r0/room_keys/version/{version}
|
// Implements GET /_matrix/client/r0/room_keys/version and GET /_matrix/client/r0/room_keys/version/{version}
|
||||||
func KeyBackupVersion(req *http.Request, userAPI userapi.ClientUserAPI, device *userapi.Device, version string) util.JSONResponse {
|
func KeyBackupVersion(req *http.Request, userAPI userapi.ClientUserAPI, device *userapi.Device, version string) util.JSONResponse {
|
||||||
var queryResp userapi.QueryKeyBackupResponse
|
queryResp, err := userAPI.QueryKeyBackup(req.Context(), &userapi.QueryKeyBackupRequest{
|
||||||
if err := userAPI.QueryKeyBackup(req.Context(), &userapi.QueryKeyBackupRequest{
|
|
||||||
UserID: device.UserID,
|
UserID: device.UserID,
|
||||||
Version: version,
|
Version: version,
|
||||||
}, &queryResp); err != nil {
|
})
|
||||||
return jsonerror.InternalAPIError(req.Context(), err)
|
if err != nil {
|
||||||
}
|
return util.ErrorResponse(fmt.Errorf("QueryKeyBackup: %s", err))
|
||||||
if queryResp.Error != "" {
|
|
||||||
return util.ErrorResponse(fmt.Errorf("QueryKeyBackup: %s", queryResp.Error))
|
|
||||||
}
|
}
|
||||||
if !queryResp.Exists {
|
if !queryResp.Exists {
|
||||||
return util.JSONResponse{
|
return util.JSONResponse{
|
||||||
Code: 404,
|
Code: 404,
|
||||||
JSON: jsonerror.NotFound("version not found"),
|
JSON: spec.NotFound("version not found"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return util.JSONResponse{
|
return util.JSONResponse{
|
||||||
|
@ -126,31 +121,29 @@ func ModifyKeyBackupVersionAuthData(req *http.Request, userAPI userapi.ClientUse
|
||||||
if resErr != nil {
|
if resErr != nil {
|
||||||
return *resErr
|
return *resErr
|
||||||
}
|
}
|
||||||
var performKeyBackupResp userapi.PerformKeyBackupResponse
|
performKeyBackupResp, err := userAPI.UpdateBackupKeyAuthData(req.Context(), &userapi.PerformKeyBackupRequest{
|
||||||
if err := userAPI.PerformKeyBackup(req.Context(), &userapi.PerformKeyBackupRequest{
|
|
||||||
UserID: device.UserID,
|
UserID: device.UserID,
|
||||||
Version: version,
|
Version: version,
|
||||||
AuthData: kb.AuthData,
|
AuthData: kb.AuthData,
|
||||||
Algorithm: kb.Algorithm,
|
Algorithm: kb.Algorithm,
|
||||||
}, &performKeyBackupResp); err != nil {
|
})
|
||||||
return jsonerror.InternalServerError()
|
switch e := err.(type) {
|
||||||
}
|
case spec.ErrRoomKeysVersion:
|
||||||
if performKeyBackupResp.Error != "" {
|
return util.JSONResponse{
|
||||||
if performKeyBackupResp.BadInput {
|
Code: http.StatusForbidden,
|
||||||
return util.JSONResponse{
|
JSON: e,
|
||||||
Code: 400,
|
|
||||||
JSON: jsonerror.InvalidArgumentValue(performKeyBackupResp.Error),
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return util.ErrorResponse(fmt.Errorf("PerformKeyBackup: %s", performKeyBackupResp.Error))
|
case nil:
|
||||||
|
default:
|
||||||
|
return util.ErrorResponse(fmt.Errorf("PerformKeyBackup: %w", e))
|
||||||
}
|
}
|
||||||
|
|
||||||
if !performKeyBackupResp.Exists {
|
if !performKeyBackupResp.Exists {
|
||||||
return util.JSONResponse{
|
return util.JSONResponse{
|
||||||
Code: 404,
|
Code: 404,
|
||||||
JSON: jsonerror.NotFound("backup version not found"),
|
JSON: spec.NotFound("backup version not found"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Unclear what the 200 body should be
|
|
||||||
return util.JSONResponse{
|
return util.JSONResponse{
|
||||||
Code: 200,
|
Code: 200,
|
||||||
JSON: keyBackupVersionCreateResponse{
|
JSON: keyBackupVersionCreateResponse{
|
||||||
|
@ -162,35 +155,19 @@ func ModifyKeyBackupVersionAuthData(req *http.Request, userAPI userapi.ClientUse
|
||||||
// Delete a version of key backup. Version must not be empty. If the key backup was previously deleted, will return 200 OK.
|
// Delete a version of key backup. Version must not be empty. If the key backup was previously deleted, will return 200 OK.
|
||||||
// Implements DELETE /_matrix/client/r0/room_keys/version/{version}
|
// Implements DELETE /_matrix/client/r0/room_keys/version/{version}
|
||||||
func DeleteKeyBackupVersion(req *http.Request, userAPI userapi.ClientUserAPI, device *userapi.Device, version string) util.JSONResponse {
|
func DeleteKeyBackupVersion(req *http.Request, userAPI userapi.ClientUserAPI, device *userapi.Device, version string) util.JSONResponse {
|
||||||
var performKeyBackupResp userapi.PerformKeyBackupResponse
|
exists, err := userAPI.DeleteKeyBackup(req.Context(), device.UserID, version)
|
||||||
if err := userAPI.PerformKeyBackup(req.Context(), &userapi.PerformKeyBackupRequest{
|
if err != nil {
|
||||||
UserID: device.UserID,
|
return util.ErrorResponse(fmt.Errorf("DeleteKeyBackup: %s", err))
|
||||||
Version: version,
|
|
||||||
DeleteBackup: true,
|
|
||||||
}, &performKeyBackupResp); err != nil {
|
|
||||||
return jsonerror.InternalServerError()
|
|
||||||
}
|
}
|
||||||
if performKeyBackupResp.Error != "" {
|
if !exists {
|
||||||
if performKeyBackupResp.BadInput {
|
|
||||||
return util.JSONResponse{
|
|
||||||
Code: 400,
|
|
||||||
JSON: jsonerror.InvalidArgumentValue(performKeyBackupResp.Error),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return util.ErrorResponse(fmt.Errorf("PerformKeyBackup: %s", performKeyBackupResp.Error))
|
|
||||||
}
|
|
||||||
if !performKeyBackupResp.Exists {
|
|
||||||
return util.JSONResponse{
|
return util.JSONResponse{
|
||||||
Code: 404,
|
Code: 404,
|
||||||
JSON: jsonerror.NotFound("backup version not found"),
|
JSON: spec.NotFound("backup version not found"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Unclear what the 200 body should be
|
|
||||||
return util.JSONResponse{
|
return util.JSONResponse{
|
||||||
Code: 200,
|
Code: 200,
|
||||||
JSON: keyBackupVersionCreateResponse{
|
JSON: struct{}{},
|
||||||
Version: performKeyBackupResp.Version,
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -198,27 +175,26 @@ func DeleteKeyBackupVersion(req *http.Request, userAPI userapi.ClientUserAPI, de
|
||||||
func UploadBackupKeys(
|
func UploadBackupKeys(
|
||||||
req *http.Request, userAPI userapi.ClientUserAPI, device *userapi.Device, version string, keys *keyBackupSessionRequest,
|
req *http.Request, userAPI userapi.ClientUserAPI, device *userapi.Device, version string, keys *keyBackupSessionRequest,
|
||||||
) util.JSONResponse {
|
) util.JSONResponse {
|
||||||
var performKeyBackupResp userapi.PerformKeyBackupResponse
|
performKeyBackupResp, err := userAPI.UpdateBackupKeyAuthData(req.Context(), &userapi.PerformKeyBackupRequest{
|
||||||
if err := userAPI.PerformKeyBackup(req.Context(), &userapi.PerformKeyBackupRequest{
|
|
||||||
UserID: device.UserID,
|
UserID: device.UserID,
|
||||||
Version: version,
|
Version: version,
|
||||||
Keys: *keys,
|
Keys: *keys,
|
||||||
}, &performKeyBackupResp); err != nil && performKeyBackupResp.Error == "" {
|
})
|
||||||
return jsonerror.InternalServerError()
|
|
||||||
}
|
switch e := err.(type) {
|
||||||
if performKeyBackupResp.Error != "" {
|
case spec.ErrRoomKeysVersion:
|
||||||
if performKeyBackupResp.BadInput {
|
return util.JSONResponse{
|
||||||
return util.JSONResponse{
|
Code: http.StatusForbidden,
|
||||||
Code: 400,
|
JSON: e,
|
||||||
JSON: jsonerror.InvalidArgumentValue(performKeyBackupResp.Error),
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return util.ErrorResponse(fmt.Errorf("PerformKeyBackup: %s", performKeyBackupResp.Error))
|
case nil:
|
||||||
|
default:
|
||||||
|
return util.ErrorResponse(fmt.Errorf("PerformKeyBackup: %w", e))
|
||||||
}
|
}
|
||||||
if !performKeyBackupResp.Exists {
|
if !performKeyBackupResp.Exists {
|
||||||
return util.JSONResponse{
|
return util.JSONResponse{
|
||||||
Code: 404,
|
Code: 404,
|
||||||
JSON: jsonerror.NotFound("backup version not found"),
|
JSON: spec.NotFound("backup version not found"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return util.JSONResponse{
|
return util.JSONResponse{
|
||||||
|
@ -234,23 +210,20 @@ func UploadBackupKeys(
|
||||||
func GetBackupKeys(
|
func GetBackupKeys(
|
||||||
req *http.Request, userAPI userapi.ClientUserAPI, device *userapi.Device, version, roomID, sessionID string,
|
req *http.Request, userAPI userapi.ClientUserAPI, device *userapi.Device, version, roomID, sessionID string,
|
||||||
) util.JSONResponse {
|
) util.JSONResponse {
|
||||||
var queryResp userapi.QueryKeyBackupResponse
|
queryResp, err := userAPI.QueryKeyBackup(req.Context(), &userapi.QueryKeyBackupRequest{
|
||||||
if err := userAPI.QueryKeyBackup(req.Context(), &userapi.QueryKeyBackupRequest{
|
|
||||||
UserID: device.UserID,
|
UserID: device.UserID,
|
||||||
Version: version,
|
Version: version,
|
||||||
ReturnKeys: true,
|
ReturnKeys: true,
|
||||||
KeysForRoomID: roomID,
|
KeysForRoomID: roomID,
|
||||||
KeysForSessionID: sessionID,
|
KeysForSessionID: sessionID,
|
||||||
}, &queryResp); err != nil {
|
})
|
||||||
return jsonerror.InternalAPIError(req.Context(), err)
|
if err != nil {
|
||||||
}
|
return util.ErrorResponse(fmt.Errorf("QueryKeyBackup: %w", err))
|
||||||
if queryResp.Error != "" {
|
|
||||||
return util.ErrorResponse(fmt.Errorf("QueryKeyBackup: %s", queryResp.Error))
|
|
||||||
}
|
}
|
||||||
if !queryResp.Exists {
|
if !queryResp.Exists {
|
||||||
return util.JSONResponse{
|
return util.JSONResponse{
|
||||||
Code: 404,
|
Code: 404,
|
||||||
JSON: jsonerror.NotFound("version not found"),
|
JSON: spec.NotFound("version not found"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if sessionID != "" {
|
if sessionID != "" {
|
||||||
|
@ -267,17 +240,20 @@ func GetBackupKeys(
|
||||||
}
|
}
|
||||||
} else if roomID != "" {
|
} else if roomID != "" {
|
||||||
roomData, ok := queryResp.Keys[roomID]
|
roomData, ok := queryResp.Keys[roomID]
|
||||||
if ok {
|
if !ok {
|
||||||
// wrap response in "sessions"
|
// If no keys are found, then an object with an empty sessions property will be returned
|
||||||
return util.JSONResponse{
|
roomData = make(map[string]userapi.KeyBackupSession)
|
||||||
Code: 200,
|
|
||||||
JSON: struct {
|
|
||||||
Sessions map[string]userapi.KeyBackupSession `json:"sessions"`
|
|
||||||
}{
|
|
||||||
Sessions: roomData,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
// wrap response in "sessions"
|
||||||
|
return util.JSONResponse{
|
||||||
|
Code: 200,
|
||||||
|
JSON: struct {
|
||||||
|
Sessions map[string]userapi.KeyBackupSession `json:"sessions"`
|
||||||
|
}{
|
||||||
|
Sessions: roomData,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
// response is the same as the upload request
|
// response is the same as the upload request
|
||||||
var resp keyBackupSessionRequest
|
var resp keyBackupSessionRequest
|
||||||
|
@ -298,6 +274,6 @@ func GetBackupKeys(
|
||||||
}
|
}
|
||||||
return util.JSONResponse{
|
return util.JSONResponse{
|
||||||
Code: 404,
|
Code: 404,
|
||||||
JSON: jsonerror.NotFound("keys not found"),
|
JSON: spec.NotFound("keys not found"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -20,9 +20,9 @@ import (
|
||||||
"github.com/matrix-org/dendrite/clientapi/auth"
|
"github.com/matrix-org/dendrite/clientapi/auth"
|
||||||
"github.com/matrix-org/dendrite/clientapi/auth/authtypes"
|
"github.com/matrix-org/dendrite/clientapi/auth/authtypes"
|
||||||
"github.com/matrix-org/dendrite/clientapi/httputil"
|
"github.com/matrix-org/dendrite/clientapi/httputil"
|
||||||
"github.com/matrix-org/dendrite/clientapi/jsonerror"
|
|
||||||
"github.com/matrix-org/dendrite/setup/config"
|
"github.com/matrix-org/dendrite/setup/config"
|
||||||
"github.com/matrix-org/dendrite/userapi/api"
|
"github.com/matrix-org/dendrite/userapi/api"
|
||||||
|
"github.com/matrix-org/gomatrixserverlib/spec"
|
||||||
"github.com/matrix-org/util"
|
"github.com/matrix-org/util"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -32,7 +32,7 @@ type crossSigningRequest struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
func UploadCrossSigningDeviceKeys(
|
func UploadCrossSigningDeviceKeys(
|
||||||
req *http.Request, userInteractiveAuth *auth.UserInteractive,
|
req *http.Request,
|
||||||
keyserverAPI api.ClientKeyAPI, device *api.Device,
|
keyserverAPI api.ClientKeyAPI, device *api.Device,
|
||||||
accountAPI api.ClientUserAPI, cfg *config.ClientAPI,
|
accountAPI api.ClientUserAPI, cfg *config.ClientAPI,
|
||||||
) util.JSONResponse {
|
) util.JSONResponse {
|
||||||
|
@ -62,8 +62,8 @@ func UploadCrossSigningDeviceKeys(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
typePassword := auth.LoginTypePassword{
|
typePassword := auth.LoginTypePassword{
|
||||||
GetAccountByPassword: accountAPI.QueryAccountByPassword,
|
UserAPI: accountAPI,
|
||||||
Config: cfg,
|
Config: cfg,
|
||||||
}
|
}
|
||||||
if _, authErr := typePassword.Login(req.Context(), &uploadReq.Auth.PasswordRequest); authErr != nil {
|
if _, authErr := typePassword.Login(req.Context(), &uploadReq.Auth.PasswordRequest); authErr != nil {
|
||||||
return *authErr
|
return *authErr
|
||||||
|
@ -71,31 +71,29 @@ func UploadCrossSigningDeviceKeys(
|
||||||
sessions.addCompletedSessionStage(sessionID, authtypes.LoginTypePassword)
|
sessions.addCompletedSessionStage(sessionID, authtypes.LoginTypePassword)
|
||||||
|
|
||||||
uploadReq.UserID = device.UserID
|
uploadReq.UserID = device.UserID
|
||||||
if err := keyserverAPI.PerformUploadDeviceKeys(req.Context(), &uploadReq.PerformUploadDeviceKeysRequest, uploadRes); err != nil {
|
keyserverAPI.PerformUploadDeviceKeys(req.Context(), &uploadReq.PerformUploadDeviceKeysRequest, uploadRes)
|
||||||
return jsonerror.InternalAPIError(req.Context(), err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := uploadRes.Error; err != nil {
|
if err := uploadRes.Error; err != nil {
|
||||||
switch {
|
switch {
|
||||||
case err.IsInvalidSignature:
|
case err.IsInvalidSignature:
|
||||||
return util.JSONResponse{
|
return util.JSONResponse{
|
||||||
Code: http.StatusBadRequest,
|
Code: http.StatusBadRequest,
|
||||||
JSON: jsonerror.InvalidSignature(err.Error()),
|
JSON: spec.InvalidSignature(err.Error()),
|
||||||
}
|
}
|
||||||
case err.IsMissingParam:
|
case err.IsMissingParam:
|
||||||
return util.JSONResponse{
|
return util.JSONResponse{
|
||||||
Code: http.StatusBadRequest,
|
Code: http.StatusBadRequest,
|
||||||
JSON: jsonerror.MissingParam(err.Error()),
|
JSON: spec.MissingParam(err.Error()),
|
||||||
}
|
}
|
||||||
case err.IsInvalidParam:
|
case err.IsInvalidParam:
|
||||||
return util.JSONResponse{
|
return util.JSONResponse{
|
||||||
Code: http.StatusBadRequest,
|
Code: http.StatusBadRequest,
|
||||||
JSON: jsonerror.InvalidParam(err.Error()),
|
JSON: spec.InvalidParam(err.Error()),
|
||||||
}
|
}
|
||||||
default:
|
default:
|
||||||
return util.JSONResponse{
|
return util.JSONResponse{
|
||||||
Code: http.StatusBadRequest,
|
Code: http.StatusBadRequest,
|
||||||
JSON: jsonerror.Unknown(err.Error()),
|
JSON: spec.Unknown(err.Error()),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -115,31 +113,29 @@ func UploadCrossSigningDeviceSignatures(req *http.Request, keyserverAPI api.Clie
|
||||||
}
|
}
|
||||||
|
|
||||||
uploadReq.UserID = device.UserID
|
uploadReq.UserID = device.UserID
|
||||||
if err := keyserverAPI.PerformUploadDeviceSignatures(req.Context(), uploadReq, uploadRes); err != nil {
|
keyserverAPI.PerformUploadDeviceSignatures(req.Context(), uploadReq, uploadRes)
|
||||||
return jsonerror.InternalAPIError(req.Context(), err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := uploadRes.Error; err != nil {
|
if err := uploadRes.Error; err != nil {
|
||||||
switch {
|
switch {
|
||||||
case err.IsInvalidSignature:
|
case err.IsInvalidSignature:
|
||||||
return util.JSONResponse{
|
return util.JSONResponse{
|
||||||
Code: http.StatusBadRequest,
|
Code: http.StatusBadRequest,
|
||||||
JSON: jsonerror.InvalidSignature(err.Error()),
|
JSON: spec.InvalidSignature(err.Error()),
|
||||||
}
|
}
|
||||||
case err.IsMissingParam:
|
case err.IsMissingParam:
|
||||||
return util.JSONResponse{
|
return util.JSONResponse{
|
||||||
Code: http.StatusBadRequest,
|
Code: http.StatusBadRequest,
|
||||||
JSON: jsonerror.MissingParam(err.Error()),
|
JSON: spec.MissingParam(err.Error()),
|
||||||
}
|
}
|
||||||
case err.IsInvalidParam:
|
case err.IsInvalidParam:
|
||||||
return util.JSONResponse{
|
return util.JSONResponse{
|
||||||
Code: http.StatusBadRequest,
|
Code: http.StatusBadRequest,
|
||||||
JSON: jsonerror.InvalidParam(err.Error()),
|
JSON: spec.InvalidParam(err.Error()),
|
||||||
}
|
}
|
||||||
default:
|
default:
|
||||||
return util.JSONResponse{
|
return util.JSONResponse{
|
||||||
Code: http.StatusBadRequest,
|
Code: http.StatusBadRequest,
|
||||||
JSON: jsonerror.Unknown(err.Error()),
|
JSON: spec.Unknown(err.Error()),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -22,8 +22,8 @@ import (
|
||||||
"github.com/matrix-org/util"
|
"github.com/matrix-org/util"
|
||||||
|
|
||||||
"github.com/matrix-org/dendrite/clientapi/httputil"
|
"github.com/matrix-org/dendrite/clientapi/httputil"
|
||||||
"github.com/matrix-org/dendrite/clientapi/jsonerror"
|
|
||||||
"github.com/matrix-org/dendrite/userapi/api"
|
"github.com/matrix-org/dendrite/userapi/api"
|
||||||
|
"github.com/matrix-org/gomatrixserverlib/spec"
|
||||||
)
|
)
|
||||||
|
|
||||||
type uploadKeysRequest struct {
|
type uploadKeysRequest struct {
|
||||||
|
@ -67,7 +67,10 @@ func UploadKeys(req *http.Request, keyAPI api.ClientKeyAPI, device *api.Device)
|
||||||
}
|
}
|
||||||
if uploadRes.Error != nil {
|
if uploadRes.Error != nil {
|
||||||
util.GetLogger(req.Context()).WithError(uploadRes.Error).Error("Failed to PerformUploadKeys")
|
util.GetLogger(req.Context()).WithError(uploadRes.Error).Error("Failed to PerformUploadKeys")
|
||||||
return jsonerror.InternalServerError()
|
return util.JSONResponse{
|
||||||
|
Code: http.StatusInternalServerError,
|
||||||
|
JSON: spec.InternalServerError{},
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if len(uploadRes.KeyErrors) > 0 {
|
if len(uploadRes.KeyErrors) > 0 {
|
||||||
util.GetLogger(req.Context()).WithField("key_errors", uploadRes.KeyErrors).Error("Failed to upload one or more keys")
|
util.GetLogger(req.Context()).WithField("key_errors", uploadRes.KeyErrors).Error("Failed to upload one or more keys")
|
||||||
|
@ -90,7 +93,6 @@ func UploadKeys(req *http.Request, keyAPI api.ClientKeyAPI, device *api.Device)
|
||||||
|
|
||||||
type queryKeysRequest struct {
|
type queryKeysRequest struct {
|
||||||
Timeout int `json:"timeout"`
|
Timeout int `json:"timeout"`
|
||||||
Token string `json:"token"`
|
|
||||||
DeviceKeys map[string][]string `json:"device_keys"`
|
DeviceKeys map[string][]string `json:"device_keys"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -112,14 +114,11 @@ func QueryKeys(req *http.Request, keyAPI api.ClientKeyAPI, device *api.Device) u
|
||||||
return *resErr
|
return *resErr
|
||||||
}
|
}
|
||||||
queryRes := api.QueryKeysResponse{}
|
queryRes := api.QueryKeysResponse{}
|
||||||
if err := keyAPI.QueryKeys(req.Context(), &api.QueryKeysRequest{
|
keyAPI.QueryKeys(req.Context(), &api.QueryKeysRequest{
|
||||||
UserID: device.UserID,
|
UserID: device.UserID,
|
||||||
UserToDevices: r.DeviceKeys,
|
UserToDevices: r.DeviceKeys,
|
||||||
Timeout: r.GetTimeout(),
|
Timeout: r.GetTimeout(),
|
||||||
// TODO: Token?
|
}, &queryRes)
|
||||||
}, &queryRes); err != nil {
|
|
||||||
return util.ErrorResponse(err)
|
|
||||||
}
|
|
||||||
return util.JSONResponse{
|
return util.JSONResponse{
|
||||||
Code: 200,
|
Code: 200,
|
||||||
JSON: map[string]interface{}{
|
JSON: map[string]interface{}{
|
||||||
|
@ -152,15 +151,16 @@ func ClaimKeys(req *http.Request, keyAPI api.ClientKeyAPI) util.JSONResponse {
|
||||||
return *resErr
|
return *resErr
|
||||||
}
|
}
|
||||||
claimRes := api.PerformClaimKeysResponse{}
|
claimRes := api.PerformClaimKeysResponse{}
|
||||||
if err := keyAPI.PerformClaimKeys(req.Context(), &api.PerformClaimKeysRequest{
|
keyAPI.PerformClaimKeys(req.Context(), &api.PerformClaimKeysRequest{
|
||||||
OneTimeKeys: r.OneTimeKeys,
|
OneTimeKeys: r.OneTimeKeys,
|
||||||
Timeout: r.GetTimeout(),
|
Timeout: r.GetTimeout(),
|
||||||
}, &claimRes); err != nil {
|
}, &claimRes)
|
||||||
return jsonerror.InternalAPIError(req.Context(), err)
|
|
||||||
}
|
|
||||||
if claimRes.Error != nil {
|
if claimRes.Error != nil {
|
||||||
util.GetLogger(req.Context()).WithError(claimRes.Error).Error("failed to PerformClaimKeys")
|
util.GetLogger(req.Context()).WithError(claimRes.Error).Error("failed to PerformClaimKeys")
|
||||||
return jsonerror.InternalServerError()
|
return util.JSONResponse{
|
||||||
|
Code: http.StatusInternalServerError,
|
||||||
|
JSON: spec.InternalServerError{},
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return util.JSONResponse{
|
return util.JSONResponse{
|
||||||
Code: 200,
|
Code: 200,
|
||||||
|
|
|
@ -17,9 +17,9 @@ package routing
|
||||||
import (
|
import (
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
"github.com/matrix-org/dendrite/clientapi/jsonerror"
|
|
||||||
roomserverAPI "github.com/matrix-org/dendrite/roomserver/api"
|
roomserverAPI "github.com/matrix-org/dendrite/roomserver/api"
|
||||||
"github.com/matrix-org/dendrite/userapi/api"
|
"github.com/matrix-org/dendrite/userapi/api"
|
||||||
|
"github.com/matrix-org/gomatrixserverlib/spec"
|
||||||
"github.com/matrix-org/util"
|
"github.com/matrix-org/util"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -29,10 +29,18 @@ func LeaveRoomByID(
|
||||||
rsAPI roomserverAPI.ClientRoomserverAPI,
|
rsAPI roomserverAPI.ClientRoomserverAPI,
|
||||||
roomID string,
|
roomID string,
|
||||||
) util.JSONResponse {
|
) util.JSONResponse {
|
||||||
|
userID, err := spec.NewUserID(device.UserID, true)
|
||||||
|
if err != nil {
|
||||||
|
return util.JSONResponse{
|
||||||
|
Code: http.StatusBadRequest,
|
||||||
|
JSON: spec.Unknown("device userID is invalid"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Prepare to ask the roomserver to perform the room join.
|
// Prepare to ask the roomserver to perform the room join.
|
||||||
leaveReq := roomserverAPI.PerformLeaveRequest{
|
leaveReq := roomserverAPI.PerformLeaveRequest{
|
||||||
RoomID: roomID,
|
RoomID: roomID,
|
||||||
UserID: device.UserID,
|
Leaver: *userID,
|
||||||
}
|
}
|
||||||
leaveRes := roomserverAPI.PerformLeaveResponse{}
|
leaveRes := roomserverAPI.PerformLeaveResponse{}
|
||||||
|
|
||||||
|
@ -41,12 +49,12 @@ func LeaveRoomByID(
|
||||||
if leaveRes.Code != 0 {
|
if leaveRes.Code != 0 {
|
||||||
return util.JSONResponse{
|
return util.JSONResponse{
|
||||||
Code: leaveRes.Code,
|
Code: leaveRes.Code,
|
||||||
JSON: jsonerror.LeaveServerNoticeError(),
|
JSON: spec.LeaveServerNoticeError(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return util.JSONResponse{
|
return util.JSONResponse{
|
||||||
Code: http.StatusBadRequest,
|
Code: http.StatusBadRequest,
|
||||||
JSON: jsonerror.Unknown(err.Error()),
|
JSON: spec.Unknown(err.Error()),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -19,10 +19,11 @@ import (
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
"github.com/matrix-org/dendrite/clientapi/auth"
|
"github.com/matrix-org/dendrite/clientapi/auth"
|
||||||
"github.com/matrix-org/dendrite/clientapi/jsonerror"
|
"github.com/matrix-org/dendrite/clientapi/auth/authtypes"
|
||||||
"github.com/matrix-org/dendrite/clientapi/userutil"
|
"github.com/matrix-org/dendrite/clientapi/userutil"
|
||||||
"github.com/matrix-org/dendrite/setup/config"
|
"github.com/matrix-org/dendrite/setup/config"
|
||||||
userapi "github.com/matrix-org/dendrite/userapi/api"
|
userapi "github.com/matrix-org/dendrite/userapi/api"
|
||||||
|
"github.com/matrix-org/gomatrixserverlib/spec"
|
||||||
"github.com/matrix-org/util"
|
"github.com/matrix-org/util"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -40,28 +41,25 @@ type flow struct {
|
||||||
Type string `json:"type"`
|
Type string `json:"type"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func passwordLogin() flows {
|
|
||||||
f := flows{}
|
|
||||||
s := flow{
|
|
||||||
Type: "m.login.password",
|
|
||||||
}
|
|
||||||
f.Flows = append(f.Flows, s)
|
|
||||||
return f
|
|
||||||
}
|
|
||||||
|
|
||||||
// Login implements GET and POST /login
|
// Login implements GET and POST /login
|
||||||
func Login(
|
func Login(
|
||||||
req *http.Request, userAPI userapi.ClientUserAPI,
|
req *http.Request, userAPI userapi.ClientUserAPI,
|
||||||
cfg *config.ClientAPI,
|
cfg *config.ClientAPI,
|
||||||
) util.JSONResponse {
|
) util.JSONResponse {
|
||||||
if req.Method == http.MethodGet {
|
if req.Method == http.MethodGet {
|
||||||
// TODO: support other forms of login other than password, depending on config options
|
loginFlows := []flow{{Type: authtypes.LoginTypePassword}}
|
||||||
|
if len(cfg.Derived.ApplicationServices) > 0 {
|
||||||
|
loginFlows = append(loginFlows, flow{Type: authtypes.LoginTypeApplicationService})
|
||||||
|
}
|
||||||
|
// TODO: support other forms of login, depending on config options
|
||||||
return util.JSONResponse{
|
return util.JSONResponse{
|
||||||
Code: http.StatusOK,
|
Code: http.StatusOK,
|
||||||
JSON: passwordLogin(),
|
JSON: flows{
|
||||||
|
Flows: loginFlows,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
} else if req.Method == http.MethodPost {
|
} else if req.Method == http.MethodPost {
|
||||||
login, cleanup, authErr := auth.LoginFromJSONReader(req.Context(), req.Body, userAPI, userAPI, cfg)
|
login, cleanup, authErr := auth.LoginFromJSONReader(req, userAPI, userAPI, cfg)
|
||||||
if authErr != nil {
|
if authErr != nil {
|
||||||
return *authErr
|
return *authErr
|
||||||
}
|
}
|
||||||
|
@ -72,7 +70,7 @@ func Login(
|
||||||
}
|
}
|
||||||
return util.JSONResponse{
|
return util.JSONResponse{
|
||||||
Code: http.StatusMethodNotAllowed,
|
Code: http.StatusMethodNotAllowed,
|
||||||
JSON: jsonerror.NotFound("Bad method"),
|
JSON: spec.NotFound("Bad method"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -83,13 +81,19 @@ func completeAuth(
|
||||||
token, err := auth.GenerateAccessToken()
|
token, err := auth.GenerateAccessToken()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
util.GetLogger(ctx).WithError(err).Error("auth.GenerateAccessToken failed")
|
util.GetLogger(ctx).WithError(err).Error("auth.GenerateAccessToken failed")
|
||||||
return jsonerror.InternalServerError()
|
return util.JSONResponse{
|
||||||
|
Code: http.StatusInternalServerError,
|
||||||
|
JSON: spec.InternalServerError{},
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
localpart, serverName, err := userutil.ParseUsernameParam(login.Username(), cfg)
|
localpart, serverName, err := userutil.ParseUsernameParam(login.Username(), cfg)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
util.GetLogger(ctx).WithError(err).Error("auth.ParseUsernameParam failed")
|
util.GetLogger(ctx).WithError(err).Error("auth.ParseUsernameParam failed")
|
||||||
return jsonerror.InternalServerError()
|
return util.JSONResponse{
|
||||||
|
Code: http.StatusInternalServerError,
|
||||||
|
JSON: spec.InternalServerError{},
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var performRes userapi.PerformDeviceCreationResponse
|
var performRes userapi.PerformDeviceCreationResponse
|
||||||
|
@ -105,7 +109,7 @@ func completeAuth(
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return util.JSONResponse{
|
return util.JSONResponse{
|
||||||
Code: http.StatusInternalServerError,
|
Code: http.StatusInternalServerError,
|
||||||
JSON: jsonerror.Unknown("failed to create device: " + err.Error()),
|
JSON: spec.Unknown("failed to create device: " + err.Error()),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -7,11 +7,17 @@ import (
|
||||||
"net/http/httptest"
|
"net/http/httptest"
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/matrix-org/dendrite/clientapi/auth/authtypes"
|
"github.com/matrix-org/dendrite/clientapi/auth/authtypes"
|
||||||
|
"github.com/matrix-org/dendrite/internal/caching"
|
||||||
|
"github.com/matrix-org/dendrite/internal/httputil"
|
||||||
|
"github.com/matrix-org/dendrite/internal/sqlutil"
|
||||||
"github.com/matrix-org/dendrite/roomserver"
|
"github.com/matrix-org/dendrite/roomserver"
|
||||||
"github.com/matrix-org/dendrite/setup/config"
|
"github.com/matrix-org/dendrite/setup/config"
|
||||||
|
"github.com/matrix-org/dendrite/setup/jetstream"
|
||||||
"github.com/matrix-org/gomatrixserverlib"
|
"github.com/matrix-org/gomatrixserverlib"
|
||||||
|
"github.com/matrix-org/gomatrixserverlib/fclient"
|
||||||
"github.com/matrix-org/util"
|
"github.com/matrix-org/util"
|
||||||
|
|
||||||
"github.com/matrix-org/dendrite/test"
|
"github.com/matrix-org/dendrite/test"
|
||||||
|
@ -28,20 +34,25 @@ func TestLogin(t *testing.T) {
|
||||||
|
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
test.WithAllDatabases(t, func(t *testing.T, dbType test.DBType) {
|
test.WithAllDatabases(t, func(t *testing.T, dbType test.DBType) {
|
||||||
base, baseClose := testrig.CreateBaseDendrite(t, dbType)
|
cfg, processCtx, close := testrig.CreateConfig(t, dbType)
|
||||||
defer baseClose()
|
defer close()
|
||||||
base.Cfg.ClientAPI.RateLimiting.Enabled = false
|
cfg.ClientAPI.RateLimiting.Enabled = false
|
||||||
|
natsInstance := jetstream.NATSInstance{}
|
||||||
// add a vhost
|
// add a vhost
|
||||||
base.Cfg.Global.VirtualHosts = append(base.Cfg.Global.VirtualHosts, &config.VirtualHost{
|
cfg.Global.VirtualHosts = append(cfg.Global.VirtualHosts, &config.VirtualHost{
|
||||||
SigningIdentity: gomatrixserverlib.SigningIdentity{ServerName: "vh1"},
|
SigningIdentity: fclient.SigningIdentity{ServerName: "vh1"},
|
||||||
})
|
})
|
||||||
|
|
||||||
rsAPI := roomserver.NewInternalAPI(base)
|
cm := sqlutil.NewConnectionManager(processCtx, cfg.Global.DatabaseOptions)
|
||||||
|
routers := httputil.NewRouters()
|
||||||
|
caches := caching.NewRistrettoCache(128*1024*1024, time.Hour, caching.DisableMetrics)
|
||||||
|
rsAPI := roomserver.NewInternalAPI(processCtx, cfg, cm, &natsInstance, caches, caching.DisableMetrics)
|
||||||
|
rsAPI.SetFederationAPI(nil, nil)
|
||||||
// Needed for /login
|
// Needed for /login
|
||||||
userAPI := userapi.NewInternalAPI(base, rsAPI, nil)
|
userAPI := userapi.NewInternalAPI(processCtx, cfg, cm, &natsInstance, rsAPI, nil, caching.DisableMetrics, testIsBlacklistedOrBackingOff)
|
||||||
|
|
||||||
// We mostly need the userAPI for this test, so nil for other APIs/caches etc.
|
// We mostly need the userAPI for this test, so nil for other APIs/caches etc.
|
||||||
Setup(base, &base.Cfg.ClientAPI, nil, nil, userAPI, nil, nil, nil, nil, nil, nil, &base.Cfg.MSCs, nil)
|
Setup(routers, cfg, nil, nil, userAPI, nil, nil, nil, nil, nil, nil, nil, caching.DisableMetrics)
|
||||||
|
|
||||||
// Create password
|
// Create password
|
||||||
password := util.RandomString(8)
|
password := util.RandomString(8)
|
||||||
|
@ -103,6 +114,44 @@ func TestLogin(t *testing.T) {
|
||||||
|
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
|
|
||||||
|
// Inject a dummy application service, so we have a "m.login.application_service"
|
||||||
|
// in the login flows
|
||||||
|
as := &config.ApplicationService{}
|
||||||
|
cfg.AppServiceAPI.Derived.ApplicationServices = []config.ApplicationService{*as}
|
||||||
|
|
||||||
|
t.Run("Supported log-in flows are returned", func(t *testing.T) {
|
||||||
|
req := test.NewRequest(t, http.MethodGet, "/_matrix/client/v3/login")
|
||||||
|
rec := httptest.NewRecorder()
|
||||||
|
routers.Client.ServeHTTP(rec, req)
|
||||||
|
if rec.Code != http.StatusOK {
|
||||||
|
t.Fatalf("failed to get log-in flows: %s", rec.Body.String())
|
||||||
|
}
|
||||||
|
|
||||||
|
t.Logf("response: %s", rec.Body.String())
|
||||||
|
resp := flows{}
|
||||||
|
if err := json.Unmarshal(rec.Body.Bytes(), &resp); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
appServiceFound := false
|
||||||
|
passwordFound := false
|
||||||
|
for _, flow := range resp.Flows {
|
||||||
|
if flow.Type == "m.login.password" {
|
||||||
|
passwordFound = true
|
||||||
|
} else if flow.Type == "m.login.application_service" {
|
||||||
|
appServiceFound = true
|
||||||
|
} else {
|
||||||
|
t.Fatalf("got unknown login flow: %s", flow.Type)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !appServiceFound {
|
||||||
|
t.Fatal("m.login.application_service missing from login flows")
|
||||||
|
}
|
||||||
|
if !passwordFound {
|
||||||
|
t.Fatal("m.login.password missing from login flows")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
for _, tc := range testCases {
|
for _, tc := range testCases {
|
||||||
t.Run(tc.name, func(t *testing.T) {
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
req := test.NewRequest(t, http.MethodPost, "/_matrix/client/v3/login", test.WithJSONBody(t, map[string]interface{}{
|
req := test.NewRequest(t, http.MethodPost, "/_matrix/client/v3/login", test.WithJSONBody(t, map[string]interface{}{
|
||||||
|
@ -114,7 +163,7 @@ func TestLogin(t *testing.T) {
|
||||||
"password": password,
|
"password": password,
|
||||||
}))
|
}))
|
||||||
rec := httptest.NewRecorder()
|
rec := httptest.NewRecorder()
|
||||||
base.PublicClientAPIMux.ServeHTTP(rec, req)
|
routers.Client.ServeHTTP(rec, req)
|
||||||
if tc.wantOK && rec.Code != http.StatusOK {
|
if tc.wantOK && rec.Code != http.StatusOK {
|
||||||
t.Fatalf("failed to login: %s", rec.Body.String())
|
t.Fatalf("failed to login: %s", rec.Body.String())
|
||||||
}
|
}
|
||||||
|
|
|
@ -17,8 +17,8 @@ package routing
|
||||||
import (
|
import (
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
"github.com/matrix-org/dendrite/clientapi/jsonerror"
|
|
||||||
"github.com/matrix-org/dendrite/userapi/api"
|
"github.com/matrix-org/dendrite/userapi/api"
|
||||||
|
"github.com/matrix-org/gomatrixserverlib/spec"
|
||||||
"github.com/matrix-org/util"
|
"github.com/matrix-org/util"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -33,7 +33,10 @@ func Logout(
|
||||||
}, &performRes)
|
}, &performRes)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
util.GetLogger(req.Context()).WithError(err).Error("PerformDeviceDeletion failed")
|
util.GetLogger(req.Context()).WithError(err).Error("PerformDeviceDeletion failed")
|
||||||
return jsonerror.InternalServerError()
|
return util.JSONResponse{
|
||||||
|
Code: http.StatusInternalServerError,
|
||||||
|
JSON: spec.InternalServerError{},
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return util.JSONResponse{
|
return util.JSONResponse{
|
||||||
|
@ -53,7 +56,10 @@ func LogoutAll(
|
||||||
}, &performRes)
|
}, &performRes)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
util.GetLogger(req.Context()).WithError(err).Error("PerformDeviceDeletion failed")
|
util.GetLogger(req.Context()).WithError(err).Error("PerformDeviceDeletion failed")
|
||||||
return jsonerror.InternalServerError()
|
return util.JSONResponse{
|
||||||
|
Code: http.StatusInternalServerError,
|
||||||
|
JSON: spec.InternalServerError{},
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return util.JSONResponse{
|
return util.JSONResponse{
|
||||||
|
|
|
@ -16,100 +16,109 @@ package routing
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"errors"
|
"crypto/ed25519"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/getsentry/sentry-go"
|
||||||
appserviceAPI "github.com/matrix-org/dendrite/appservice/api"
|
appserviceAPI "github.com/matrix-org/dendrite/appservice/api"
|
||||||
"github.com/matrix-org/dendrite/clientapi/auth/authtypes"
|
"github.com/matrix-org/dendrite/clientapi/auth/authtypes"
|
||||||
"github.com/matrix-org/dendrite/clientapi/httputil"
|
"github.com/matrix-org/dendrite/clientapi/httputil"
|
||||||
"github.com/matrix-org/dendrite/clientapi/jsonerror"
|
|
||||||
"github.com/matrix-org/dendrite/clientapi/threepid"
|
"github.com/matrix-org/dendrite/clientapi/threepid"
|
||||||
"github.com/matrix-org/dendrite/internal/eventutil"
|
"github.com/matrix-org/dendrite/internal/eventutil"
|
||||||
"github.com/matrix-org/dendrite/roomserver/api"
|
"github.com/matrix-org/dendrite/roomserver/api"
|
||||||
roomserverAPI "github.com/matrix-org/dendrite/roomserver/api"
|
roomserverAPI "github.com/matrix-org/dendrite/roomserver/api"
|
||||||
|
"github.com/matrix-org/dendrite/roomserver/types"
|
||||||
"github.com/matrix-org/dendrite/setup/config"
|
"github.com/matrix-org/dendrite/setup/config"
|
||||||
userapi "github.com/matrix-org/dendrite/userapi/api"
|
userapi "github.com/matrix-org/dendrite/userapi/api"
|
||||||
"github.com/matrix-org/gomatrixserverlib"
|
"github.com/matrix-org/gomatrixserverlib"
|
||||||
|
"github.com/matrix-org/gomatrixserverlib/fclient"
|
||||||
|
"github.com/matrix-org/gomatrixserverlib/spec"
|
||||||
|
|
||||||
"github.com/matrix-org/util"
|
"github.com/matrix-org/util"
|
||||||
)
|
)
|
||||||
|
|
||||||
var errMissingUserID = errors.New("'user_id' must be supplied")
|
|
||||||
|
|
||||||
func SendBan(
|
func SendBan(
|
||||||
req *http.Request, profileAPI userapi.ClientUserAPI, device *userapi.Device,
|
req *http.Request, profileAPI userapi.ClientUserAPI, device *userapi.Device,
|
||||||
roomID string, cfg *config.ClientAPI,
|
roomID string, cfg *config.ClientAPI,
|
||||||
rsAPI roomserverAPI.ClientRoomserverAPI, asAPI appserviceAPI.AppServiceInternalAPI,
|
rsAPI roomserverAPI.ClientRoomserverAPI, asAPI appserviceAPI.AppServiceInternalAPI,
|
||||||
) util.JSONResponse {
|
) util.JSONResponse {
|
||||||
body, evTime, roomVer, reqErr := extractRequestData(req, roomID, rsAPI)
|
body, evTime, reqErr := extractRequestData(req)
|
||||||
if reqErr != nil {
|
if reqErr != nil {
|
||||||
return *reqErr
|
return *reqErr
|
||||||
}
|
}
|
||||||
|
|
||||||
errRes := checkMemberInRoom(req.Context(), rsAPI, device.UserID, roomID)
|
if body.UserID == "" {
|
||||||
|
return util.JSONResponse{
|
||||||
|
Code: http.StatusBadRequest,
|
||||||
|
JSON: spec.BadJSON("missing user_id"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
deviceUserID, err := spec.NewUserID(device.UserID, true)
|
||||||
|
if err != nil {
|
||||||
|
return util.JSONResponse{
|
||||||
|
Code: http.StatusForbidden,
|
||||||
|
JSON: spec.Forbidden("You don't have permission to ban this user, bad userID"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
validRoomID, err := spec.NewRoomID(roomID)
|
||||||
|
if err != nil {
|
||||||
|
return util.JSONResponse{
|
||||||
|
Code: http.StatusBadRequest,
|
||||||
|
JSON: spec.BadJSON("RoomID is invalid"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
senderID, err := rsAPI.QuerySenderIDForUser(req.Context(), *validRoomID, *deviceUserID)
|
||||||
|
if err != nil || senderID == nil {
|
||||||
|
return util.JSONResponse{
|
||||||
|
Code: http.StatusForbidden,
|
||||||
|
JSON: spec.Forbidden("You don't have permission to ban this user, unknown senderID"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
errRes := checkMemberInRoom(req.Context(), rsAPI, *deviceUserID, roomID)
|
||||||
if errRes != nil {
|
if errRes != nil {
|
||||||
return *errRes
|
return *errRes
|
||||||
}
|
}
|
||||||
|
|
||||||
plEvent := roomserverAPI.GetStateEvent(req.Context(), rsAPI, roomID, gomatrixserverlib.StateKeyTuple{
|
pl, errRes := getPowerlevels(req, rsAPI, roomID)
|
||||||
EventType: gomatrixserverlib.MRoomPowerLevels,
|
if errRes != nil {
|
||||||
StateKey: "",
|
return *errRes
|
||||||
})
|
|
||||||
if plEvent == nil {
|
|
||||||
return util.JSONResponse{
|
|
||||||
Code: 403,
|
|
||||||
JSON: jsonerror.Forbidden("You don't have permission to ban this user, no power_levels event in this room."),
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
pl, err := plEvent.PowerLevels()
|
allowedToBan := pl.UserLevel(*senderID) >= pl.Ban
|
||||||
if err != nil {
|
|
||||||
return util.JSONResponse{
|
|
||||||
Code: 403,
|
|
||||||
JSON: jsonerror.Forbidden("You don't have permission to ban this user, the power_levels event for this room is malformed so auth checks cannot be performed."),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
allowedToBan := pl.UserLevel(device.UserID) >= pl.Ban
|
|
||||||
if !allowedToBan {
|
if !allowedToBan {
|
||||||
return util.JSONResponse{
|
return util.JSONResponse{
|
||||||
Code: 403,
|
Code: http.StatusForbidden,
|
||||||
JSON: jsonerror.Forbidden("You don't have permission to ban this user, power level too low."),
|
JSON: spec.Forbidden("You don't have permission to ban this user, power level too low."),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return sendMembership(req.Context(), profileAPI, device, roomID, "ban", body.Reason, cfg, body.UserID, evTime, roomVer, rsAPI, asAPI)
|
return sendMembership(req.Context(), profileAPI, device, roomID, spec.Ban, body.Reason, cfg, body.UserID, evTime, rsAPI, asAPI)
|
||||||
}
|
}
|
||||||
|
|
||||||
func sendMembership(ctx context.Context, profileAPI userapi.ClientUserAPI, device *userapi.Device,
|
func sendMembership(ctx context.Context, profileAPI userapi.ClientUserAPI, device *userapi.Device,
|
||||||
roomID, membership, reason string, cfg *config.ClientAPI, targetUserID string, evTime time.Time,
|
roomID, membership, reason string, cfg *config.ClientAPI, targetUserID string, evTime time.Time,
|
||||||
roomVer gomatrixserverlib.RoomVersion,
|
|
||||||
rsAPI roomserverAPI.ClientRoomserverAPI, asAPI appserviceAPI.AppServiceInternalAPI) util.JSONResponse {
|
rsAPI roomserverAPI.ClientRoomserverAPI, asAPI appserviceAPI.AppServiceInternalAPI) util.JSONResponse {
|
||||||
|
|
||||||
event, err := buildMembershipEvent(
|
event, err := buildMembershipEvent(
|
||||||
ctx, targetUserID, reason, profileAPI, device, membership,
|
ctx, targetUserID, reason, profileAPI, device, membership,
|
||||||
roomID, false, cfg, evTime, rsAPI, asAPI,
|
roomID, false, cfg, evTime, rsAPI, asAPI,
|
||||||
)
|
)
|
||||||
if err == errMissingUserID {
|
if err != nil {
|
||||||
return util.JSONResponse{
|
|
||||||
Code: http.StatusBadRequest,
|
|
||||||
JSON: jsonerror.BadJSON(err.Error()),
|
|
||||||
}
|
|
||||||
} else if err == eventutil.ErrRoomNoExists {
|
|
||||||
return util.JSONResponse{
|
|
||||||
Code: http.StatusNotFound,
|
|
||||||
JSON: jsonerror.NotFound(err.Error()),
|
|
||||||
}
|
|
||||||
} else if err != nil {
|
|
||||||
util.GetLogger(ctx).WithError(err).Error("buildMembershipEvent failed")
|
util.GetLogger(ctx).WithError(err).Error("buildMembershipEvent failed")
|
||||||
return jsonerror.InternalServerError()
|
return util.JSONResponse{
|
||||||
|
Code: http.StatusInternalServerError,
|
||||||
|
JSON: spec.InternalServerError{},
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
serverName := device.UserDomain()
|
serverName := device.UserDomain()
|
||||||
if err = roomserverAPI.SendEvents(
|
if err = roomserverAPI.SendEvents(
|
||||||
ctx, rsAPI,
|
ctx, rsAPI,
|
||||||
roomserverAPI.KindNew,
|
roomserverAPI.KindNew,
|
||||||
[]*gomatrixserverlib.HeaderedEvent{event.Event.Headered(roomVer)},
|
[]*types.HeaderedEvent{event},
|
||||||
device.UserDomain(),
|
device.UserDomain(),
|
||||||
serverName,
|
serverName,
|
||||||
serverName,
|
serverName,
|
||||||
|
@ -117,7 +126,10 @@ func sendMembership(ctx context.Context, profileAPI userapi.ClientUserAPI, devic
|
||||||
false,
|
false,
|
||||||
); err != nil {
|
); err != nil {
|
||||||
util.GetLogger(ctx).WithError(err).Error("SendEvents failed")
|
util.GetLogger(ctx).WithError(err).Error("SendEvents failed")
|
||||||
return jsonerror.InternalServerError()
|
return util.JSONResponse{
|
||||||
|
Code: http.StatusInternalServerError,
|
||||||
|
JSON: spec.InternalServerError{},
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return util.JSONResponse{
|
return util.JSONResponse{
|
||||||
|
@ -131,39 +143,81 @@ func SendKick(
|
||||||
roomID string, cfg *config.ClientAPI,
|
roomID string, cfg *config.ClientAPI,
|
||||||
rsAPI roomserverAPI.ClientRoomserverAPI, asAPI appserviceAPI.AppServiceInternalAPI,
|
rsAPI roomserverAPI.ClientRoomserverAPI, asAPI appserviceAPI.AppServiceInternalAPI,
|
||||||
) util.JSONResponse {
|
) util.JSONResponse {
|
||||||
body, evTime, roomVer, reqErr := extractRequestData(req, roomID, rsAPI)
|
body, evTime, reqErr := extractRequestData(req)
|
||||||
if reqErr != nil {
|
if reqErr != nil {
|
||||||
return *reqErr
|
return *reqErr
|
||||||
}
|
}
|
||||||
if body.UserID == "" {
|
if body.UserID == "" {
|
||||||
return util.JSONResponse{
|
return util.JSONResponse{
|
||||||
Code: 400,
|
Code: http.StatusBadRequest,
|
||||||
JSON: jsonerror.BadJSON("missing user_id"),
|
JSON: spec.BadJSON("missing user_id"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
errRes := checkMemberInRoom(req.Context(), rsAPI, device.UserID, roomID)
|
deviceUserID, err := spec.NewUserID(device.UserID, true)
|
||||||
|
if err != nil {
|
||||||
|
return util.JSONResponse{
|
||||||
|
Code: http.StatusForbidden,
|
||||||
|
JSON: spec.Forbidden("You don't have permission to kick this user, bad userID"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
validRoomID, err := spec.NewRoomID(roomID)
|
||||||
|
if err != nil {
|
||||||
|
return util.JSONResponse{
|
||||||
|
Code: http.StatusBadRequest,
|
||||||
|
JSON: spec.BadJSON("RoomID is invalid"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
senderID, err := rsAPI.QuerySenderIDForUser(req.Context(), *validRoomID, *deviceUserID)
|
||||||
|
if err != nil || senderID == nil {
|
||||||
|
return util.JSONResponse{
|
||||||
|
Code: http.StatusForbidden,
|
||||||
|
JSON: spec.Forbidden("You don't have permission to kick this user, unknown senderID"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
errRes := checkMemberInRoom(req.Context(), rsAPI, *deviceUserID, roomID)
|
||||||
if errRes != nil {
|
if errRes != nil {
|
||||||
return *errRes
|
return *errRes
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bodyUserID, err := spec.NewUserID(body.UserID, true)
|
||||||
|
if err != nil {
|
||||||
|
return util.JSONResponse{
|
||||||
|
Code: http.StatusBadRequest,
|
||||||
|
JSON: spec.BadJSON("body userID is invalid"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pl, errRes := getPowerlevels(req, rsAPI, roomID)
|
||||||
|
if errRes != nil {
|
||||||
|
return *errRes
|
||||||
|
}
|
||||||
|
allowedToKick := pl.UserLevel(*senderID) >= pl.Kick || bodyUserID.String() == deviceUserID.String()
|
||||||
|
if !allowedToKick {
|
||||||
|
return util.JSONResponse{
|
||||||
|
Code: http.StatusForbidden,
|
||||||
|
JSON: spec.Forbidden("You don't have permission to kick this user, power level too low."),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
var queryRes roomserverAPI.QueryMembershipForUserResponse
|
var queryRes roomserverAPI.QueryMembershipForUserResponse
|
||||||
err := rsAPI.QueryMembershipForUser(req.Context(), &roomserverAPI.QueryMembershipForUserRequest{
|
err = rsAPI.QueryMembershipForUser(req.Context(), &roomserverAPI.QueryMembershipForUserRequest{
|
||||||
RoomID: roomID,
|
RoomID: roomID,
|
||||||
UserID: body.UserID,
|
UserID: *bodyUserID,
|
||||||
}, &queryRes)
|
}, &queryRes)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return util.ErrorResponse(err)
|
return util.ErrorResponse(err)
|
||||||
}
|
}
|
||||||
// kick is only valid if the user is not currently banned or left (that is, they are joined or invited)
|
// kick is only valid if the user is not currently banned or left (that is, they are joined or invited)
|
||||||
if queryRes.Membership != "join" && queryRes.Membership != "invite" {
|
if queryRes.Membership != spec.Join && queryRes.Membership != spec.Invite {
|
||||||
return util.JSONResponse{
|
return util.JSONResponse{
|
||||||
Code: 403,
|
Code: http.StatusForbidden,
|
||||||
JSON: jsonerror.Unknown("cannot /kick banned or left users"),
|
JSON: spec.Unknown("cannot /kick banned or left users"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// TODO: should we be using SendLeave instead?
|
// TODO: should we be using SendLeave instead?
|
||||||
return sendMembership(req.Context(), profileAPI, device, roomID, "leave", body.Reason, cfg, body.UserID, evTime, roomVer, rsAPI, asAPI)
|
return sendMembership(req.Context(), profileAPI, device, roomID, spec.Leave, body.Reason, cfg, body.UserID, evTime, rsAPI, asAPI)
|
||||||
}
|
}
|
||||||
|
|
||||||
func SendUnban(
|
func SendUnban(
|
||||||
|
@ -171,40 +225,55 @@ func SendUnban(
|
||||||
roomID string, cfg *config.ClientAPI,
|
roomID string, cfg *config.ClientAPI,
|
||||||
rsAPI roomserverAPI.ClientRoomserverAPI, asAPI appserviceAPI.AppServiceInternalAPI,
|
rsAPI roomserverAPI.ClientRoomserverAPI, asAPI appserviceAPI.AppServiceInternalAPI,
|
||||||
) util.JSONResponse {
|
) util.JSONResponse {
|
||||||
body, evTime, roomVer, reqErr := extractRequestData(req, roomID, rsAPI)
|
body, evTime, reqErr := extractRequestData(req)
|
||||||
if reqErr != nil {
|
if reqErr != nil {
|
||||||
return *reqErr
|
return *reqErr
|
||||||
}
|
}
|
||||||
if body.UserID == "" {
|
if body.UserID == "" {
|
||||||
return util.JSONResponse{
|
return util.JSONResponse{
|
||||||
Code: 400,
|
Code: http.StatusBadRequest,
|
||||||
JSON: jsonerror.BadJSON("missing user_id"),
|
JSON: spec.BadJSON("missing user_id"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
deviceUserID, err := spec.NewUserID(device.UserID, true)
|
||||||
|
if err != nil {
|
||||||
|
return util.JSONResponse{
|
||||||
|
Code: http.StatusForbidden,
|
||||||
|
JSON: spec.Forbidden("You don't have permission to kick this user, bad userID"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
errRes := checkMemberInRoom(req.Context(), rsAPI, *deviceUserID, roomID)
|
||||||
|
if errRes != nil {
|
||||||
|
return *errRes
|
||||||
|
}
|
||||||
|
|
||||||
|
bodyUserID, err := spec.NewUserID(body.UserID, true)
|
||||||
|
if err != nil {
|
||||||
|
return util.JSONResponse{
|
||||||
|
Code: http.StatusBadRequest,
|
||||||
|
JSON: spec.BadJSON("body userID is invalid"),
|
||||||
|
}
|
||||||
|
}
|
||||||
var queryRes roomserverAPI.QueryMembershipForUserResponse
|
var queryRes roomserverAPI.QueryMembershipForUserResponse
|
||||||
err := rsAPI.QueryMembershipForUser(req.Context(), &roomserverAPI.QueryMembershipForUserRequest{
|
err = rsAPI.QueryMembershipForUser(req.Context(), &roomserverAPI.QueryMembershipForUserRequest{
|
||||||
RoomID: roomID,
|
RoomID: roomID,
|
||||||
UserID: body.UserID,
|
UserID: *bodyUserID,
|
||||||
}, &queryRes)
|
}, &queryRes)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return util.ErrorResponse(err)
|
return util.ErrorResponse(err)
|
||||||
}
|
}
|
||||||
if !queryRes.RoomExists {
|
|
||||||
return util.JSONResponse{
|
|
||||||
Code: http.StatusForbidden,
|
|
||||||
JSON: jsonerror.Forbidden("room does not exist"),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// unban is only valid if the user is currently banned
|
// unban is only valid if the user is currently banned
|
||||||
if queryRes.Membership != "ban" {
|
if queryRes.Membership != spec.Ban {
|
||||||
return util.JSONResponse{
|
return util.JSONResponse{
|
||||||
Code: 400,
|
Code: http.StatusBadRequest,
|
||||||
JSON: jsonerror.Unknown("can only /unban users that are banned"),
|
JSON: spec.Unknown("can only /unban users that are banned"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// TODO: should we be using SendLeave instead?
|
// TODO: should we be using SendLeave instead?
|
||||||
return sendMembership(req.Context(), profileAPI, device, roomID, "leave", body.Reason, cfg, body.UserID, evTime, roomVer, rsAPI, asAPI)
|
return sendMembership(req.Context(), profileAPI, device, roomID, spec.Leave, body.Reason, cfg, body.UserID, evTime, rsAPI, asAPI)
|
||||||
}
|
}
|
||||||
|
|
||||||
func SendInvite(
|
func SendInvite(
|
||||||
|
@ -212,7 +281,7 @@ func SendInvite(
|
||||||
roomID string, cfg *config.ClientAPI,
|
roomID string, cfg *config.ClientAPI,
|
||||||
rsAPI roomserverAPI.ClientRoomserverAPI, asAPI appserviceAPI.AppServiceInternalAPI,
|
rsAPI roomserverAPI.ClientRoomserverAPI, asAPI appserviceAPI.AppServiceInternalAPI,
|
||||||
) util.JSONResponse {
|
) util.JSONResponse {
|
||||||
body, evTime, _, reqErr := extractRequestData(req, roomID, rsAPI)
|
body, evTime, reqErr := extractRequestData(req)
|
||||||
if reqErr != nil {
|
if reqErr != nil {
|
||||||
return *reqErr
|
return *reqErr
|
||||||
}
|
}
|
||||||
|
@ -234,55 +303,103 @@ func SendInvite(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if body.UserID == "" {
|
||||||
|
return util.JSONResponse{
|
||||||
|
Code: http.StatusBadRequest,
|
||||||
|
JSON: spec.BadJSON("missing user_id"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
deviceUserID, err := spec.NewUserID(device.UserID, true)
|
||||||
|
if err != nil {
|
||||||
|
return util.JSONResponse{
|
||||||
|
Code: http.StatusForbidden,
|
||||||
|
JSON: spec.Forbidden("You don't have permission to kick this user, bad userID"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
errRes := checkMemberInRoom(req.Context(), rsAPI, *deviceUserID, roomID)
|
||||||
|
if errRes != nil {
|
||||||
|
return *errRes
|
||||||
|
}
|
||||||
|
|
||||||
// We already received the return value, so no need to check for an error here.
|
// We already received the return value, so no need to check for an error here.
|
||||||
response, _ := sendInvite(req.Context(), profileAPI, device, roomID, body.UserID, body.Reason, cfg, rsAPI, asAPI, evTime)
|
response, _ := sendInvite(req.Context(), device, roomID, body.UserID, body.Reason, cfg, rsAPI, evTime)
|
||||||
return response
|
return response
|
||||||
}
|
}
|
||||||
|
|
||||||
// sendInvite sends an invitation to a user. Returns a JSONResponse and an error
|
// sendInvite sends an invitation to a user. Returns a JSONResponse and an error
|
||||||
func sendInvite(
|
func sendInvite(
|
||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
profileAPI userapi.ClientUserAPI,
|
|
||||||
device *userapi.Device,
|
device *userapi.Device,
|
||||||
roomID, userID, reason string,
|
roomID, userID, reason string,
|
||||||
cfg *config.ClientAPI,
|
cfg *config.ClientAPI,
|
||||||
rsAPI roomserverAPI.ClientRoomserverAPI,
|
rsAPI roomserverAPI.ClientRoomserverAPI,
|
||||||
asAPI appserviceAPI.AppServiceInternalAPI, evTime time.Time,
|
evTime time.Time,
|
||||||
) (util.JSONResponse, error) {
|
) (util.JSONResponse, error) {
|
||||||
event, err := buildMembershipEvent(
|
validRoomID, err := spec.NewRoomID(roomID)
|
||||||
ctx, userID, reason, profileAPI, device, "invite",
|
if err != nil {
|
||||||
roomID, false, cfg, evTime, rsAPI, asAPI,
|
|
||||||
)
|
|
||||||
if err == errMissingUserID {
|
|
||||||
return util.JSONResponse{
|
return util.JSONResponse{
|
||||||
Code: http.StatusBadRequest,
|
Code: http.StatusBadRequest,
|
||||||
JSON: jsonerror.BadJSON(err.Error()),
|
JSON: spec.InvalidParam("RoomID is invalid"),
|
||||||
}, err
|
}, err
|
||||||
} else if err == eventutil.ErrRoomNoExists {
|
|
||||||
return util.JSONResponse{
|
|
||||||
Code: http.StatusNotFound,
|
|
||||||
JSON: jsonerror.NotFound(err.Error()),
|
|
||||||
}, err
|
|
||||||
} else if err != nil {
|
|
||||||
util.GetLogger(ctx).WithError(err).Error("buildMembershipEvent failed")
|
|
||||||
return jsonerror.InternalServerError(), err
|
|
||||||
}
|
}
|
||||||
|
inviter, err := spec.NewUserID(device.UserID, true)
|
||||||
var inviteRes api.PerformInviteResponse
|
if err != nil {
|
||||||
if err := rsAPI.PerformInvite(ctx, &api.PerformInviteRequest{
|
|
||||||
Event: event,
|
|
||||||
InviteRoomState: nil, // ask the roomserver to draw up invite room state for us
|
|
||||||
RoomVersion: event.RoomVersion,
|
|
||||||
SendAsServer: string(device.UserDomain()),
|
|
||||||
}, &inviteRes); err != nil {
|
|
||||||
util.GetLogger(ctx).WithError(err).Error("PerformInvite failed")
|
|
||||||
return util.JSONResponse{
|
return util.JSONResponse{
|
||||||
Code: http.StatusInternalServerError,
|
Code: http.StatusInternalServerError,
|
||||||
JSON: jsonerror.InternalServerError(),
|
JSON: spec.InternalServerError{},
|
||||||
}, err
|
}, err
|
||||||
}
|
}
|
||||||
if inviteRes.Error != nil {
|
invitee, err := spec.NewUserID(userID, true)
|
||||||
return inviteRes.Error.JSONResponse(), inviteRes.Error
|
if err != nil {
|
||||||
|
return util.JSONResponse{
|
||||||
|
Code: http.StatusBadRequest,
|
||||||
|
JSON: spec.InvalidParam("UserID is invalid"),
|
||||||
|
}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
identity, err := cfg.Matrix.SigningIdentityFor(device.UserDomain())
|
||||||
|
if err != nil {
|
||||||
|
return util.JSONResponse{
|
||||||
|
Code: http.StatusInternalServerError,
|
||||||
|
JSON: spec.InternalServerError{},
|
||||||
|
}, err
|
||||||
|
}
|
||||||
|
err = rsAPI.PerformInvite(ctx, &api.PerformInviteRequest{
|
||||||
|
InviteInput: roomserverAPI.InviteInput{
|
||||||
|
RoomID: *validRoomID,
|
||||||
|
Inviter: *inviter,
|
||||||
|
Invitee: *invitee,
|
||||||
|
Reason: reason,
|
||||||
|
IsDirect: false,
|
||||||
|
KeyID: identity.KeyID,
|
||||||
|
PrivateKey: identity.PrivateKey,
|
||||||
|
EventTime: evTime,
|
||||||
|
},
|
||||||
|
InviteRoomState: nil, // ask the roomserver to draw up invite room state for us
|
||||||
|
SendAsServer: string(device.UserDomain()),
|
||||||
|
})
|
||||||
|
|
||||||
|
switch e := err.(type) {
|
||||||
|
case roomserverAPI.ErrInvalidID:
|
||||||
|
return util.JSONResponse{
|
||||||
|
Code: http.StatusBadRequest,
|
||||||
|
JSON: spec.Unknown(e.Error()),
|
||||||
|
}, e
|
||||||
|
case roomserverAPI.ErrNotAllowed:
|
||||||
|
return util.JSONResponse{
|
||||||
|
Code: http.StatusForbidden,
|
||||||
|
JSON: spec.Forbidden(e.Error()),
|
||||||
|
}, e
|
||||||
|
case nil:
|
||||||
|
default:
|
||||||
|
util.GetLogger(ctx).WithError(err).Error("PerformInvite failed")
|
||||||
|
sentry.CaptureException(err)
|
||||||
|
return util.JSONResponse{
|
||||||
|
Code: http.StatusInternalServerError,
|
||||||
|
JSON: spec.InternalServerError{},
|
||||||
|
}, err
|
||||||
}
|
}
|
||||||
|
|
||||||
return util.JSONResponse{
|
return util.JSONResponse{
|
||||||
|
@ -291,6 +408,42 @@ func sendInvite(
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func buildMembershipEventDirect(
|
||||||
|
ctx context.Context,
|
||||||
|
targetSenderID spec.SenderID, reason string, userDisplayName, userAvatarURL string,
|
||||||
|
sender spec.SenderID, senderDomain spec.ServerName,
|
||||||
|
membership, roomID string, isDirect bool,
|
||||||
|
keyID gomatrixserverlib.KeyID, privateKey ed25519.PrivateKey, evTime time.Time,
|
||||||
|
rsAPI roomserverAPI.ClientRoomserverAPI,
|
||||||
|
) (*types.HeaderedEvent, error) {
|
||||||
|
targetSenderString := string(targetSenderID)
|
||||||
|
proto := gomatrixserverlib.ProtoEvent{
|
||||||
|
SenderID: string(sender),
|
||||||
|
RoomID: roomID,
|
||||||
|
Type: "m.room.member",
|
||||||
|
StateKey: &targetSenderString,
|
||||||
|
}
|
||||||
|
|
||||||
|
content := gomatrixserverlib.MemberContent{
|
||||||
|
Membership: membership,
|
||||||
|
DisplayName: userDisplayName,
|
||||||
|
AvatarURL: userAvatarURL,
|
||||||
|
Reason: reason,
|
||||||
|
IsDirect: isDirect,
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := proto.SetContent(content); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
identity := &fclient.SigningIdentity{
|
||||||
|
ServerName: senderDomain,
|
||||||
|
KeyID: keyID,
|
||||||
|
PrivateKey: privateKey,
|
||||||
|
}
|
||||||
|
return eventutil.QueryAndBuildEvent(ctx, &proto, identity, evTime, rsAPI, nil)
|
||||||
|
}
|
||||||
|
|
||||||
func buildMembershipEvent(
|
func buildMembershipEvent(
|
||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
targetUserID, reason string, profileAPI userapi.ClientUserAPI,
|
targetUserID, reason string, profileAPI userapi.ClientUserAPI,
|
||||||
|
@ -298,37 +451,45 @@ func buildMembershipEvent(
|
||||||
membership, roomID string, isDirect bool,
|
membership, roomID string, isDirect bool,
|
||||||
cfg *config.ClientAPI, evTime time.Time,
|
cfg *config.ClientAPI, evTime time.Time,
|
||||||
rsAPI roomserverAPI.ClientRoomserverAPI, asAPI appserviceAPI.AppServiceInternalAPI,
|
rsAPI roomserverAPI.ClientRoomserverAPI, asAPI appserviceAPI.AppServiceInternalAPI,
|
||||||
) (*gomatrixserverlib.HeaderedEvent, error) {
|
) (*types.HeaderedEvent, error) {
|
||||||
profile, err := loadProfile(ctx, targetUserID, cfg, profileAPI, asAPI)
|
profile, err := loadProfile(ctx, targetUserID, cfg, profileAPI, asAPI)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
builder := gomatrixserverlib.EventBuilder{
|
userID, err := spec.NewUserID(device.UserID, true)
|
||||||
Sender: device.UserID,
|
if err != nil {
|
||||||
RoomID: roomID,
|
|
||||||
Type: "m.room.member",
|
|
||||||
StateKey: &targetUserID,
|
|
||||||
}
|
|
||||||
|
|
||||||
content := gomatrixserverlib.MemberContent{
|
|
||||||
Membership: membership,
|
|
||||||
DisplayName: profile.DisplayName,
|
|
||||||
AvatarURL: profile.AvatarURL,
|
|
||||||
Reason: reason,
|
|
||||||
IsDirect: isDirect,
|
|
||||||
}
|
|
||||||
|
|
||||||
if err = builder.SetContent(content); err != nil {
|
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
validRoomID, err := spec.NewRoomID(roomID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
senderID, err := rsAPI.QuerySenderIDForUser(ctx, *validRoomID, *userID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
} else if senderID == nil {
|
||||||
|
return nil, fmt.Errorf("no sender ID for %s in %s", *userID, *validRoomID)
|
||||||
|
}
|
||||||
|
|
||||||
identity, err := cfg.Matrix.SigningIdentityFor(device.UserDomain())
|
targetID, err := spec.NewUserID(targetUserID, true)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
targetSenderID, err := rsAPI.QuerySenderIDForUser(ctx, *validRoomID, *targetID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
} else if targetSenderID == nil {
|
||||||
|
return nil, fmt.Errorf("no sender ID for %s in %s", *targetID, *validRoomID)
|
||||||
|
}
|
||||||
|
|
||||||
|
identity, err := rsAPI.SigningIdentityFor(ctx, *validRoomID, *userID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
return eventutil.QueryAndBuildEvent(ctx, &builder, cfg.Matrix, identity, evTime, rsAPI, nil)
|
return buildMembershipEventDirect(ctx, *targetSenderID, reason, profile.DisplayName, profile.AvatarURL,
|
||||||
|
*senderID, device.UserDomain(), membership, roomID, isDirect, identity.KeyID, identity.PrivateKey, evTime, rsAPI)
|
||||||
}
|
}
|
||||||
|
|
||||||
// loadProfile lookups the profile of a given user from the database and returns
|
// loadProfile lookups the profile of a given user from the database and returns
|
||||||
|
@ -357,19 +518,7 @@ func loadProfile(
|
||||||
return profile, err
|
return profile, err
|
||||||
}
|
}
|
||||||
|
|
||||||
func extractRequestData(req *http.Request, roomID string, rsAPI roomserverAPI.ClientRoomserverAPI) (
|
func extractRequestData(req *http.Request) (body *threepid.MembershipRequest, evTime time.Time, resErr *util.JSONResponse) {
|
||||||
body *threepid.MembershipRequest, evTime time.Time, roomVer gomatrixserverlib.RoomVersion, resErr *util.JSONResponse,
|
|
||||||
) {
|
|
||||||
verReq := roomserverAPI.QueryRoomVersionForRoomRequest{RoomID: roomID}
|
|
||||||
verRes := roomserverAPI.QueryRoomVersionForRoomResponse{}
|
|
||||||
if err := rsAPI.QueryRoomVersionForRoom(req.Context(), &verReq, &verRes); err != nil {
|
|
||||||
resErr = &util.JSONResponse{
|
|
||||||
Code: http.StatusBadRequest,
|
|
||||||
JSON: jsonerror.UnsupportedRoomVersion(err.Error()),
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
roomVer = verRes.RoomVersion
|
|
||||||
|
|
||||||
if reqErr := httputil.UnmarshalJSONRequest(req, &body); reqErr != nil {
|
if reqErr := httputil.UnmarshalJSONRequest(req, &body); reqErr != nil {
|
||||||
resErr = reqErr
|
resErr = reqErr
|
||||||
|
@ -380,7 +529,7 @@ func extractRequestData(req *http.Request, roomID string, rsAPI roomserverAPI.Cl
|
||||||
if err != nil {
|
if err != nil {
|
||||||
resErr = &util.JSONResponse{
|
resErr = &util.JSONResponse{
|
||||||
Code: http.StatusBadRequest,
|
Code: http.StatusBadRequest,
|
||||||
JSON: jsonerror.InvalidArgumentValue(err.Error()),
|
JSON: spec.InvalidParam(err.Error()),
|
||||||
}
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@ -402,67 +551,59 @@ func checkAndProcessThreepid(
|
||||||
req.Context(), device, body, cfg, rsAPI, profileAPI,
|
req.Context(), device, body, cfg, rsAPI, profileAPI,
|
||||||
roomID, evTime,
|
roomID, evTime,
|
||||||
)
|
)
|
||||||
if err == threepid.ErrMissingParameter {
|
switch e := err.(type) {
|
||||||
|
case nil:
|
||||||
|
case threepid.ErrMissingParameter:
|
||||||
|
util.GetLogger(req.Context()).WithError(err).Error("threepid.CheckAndProcessInvite failed")
|
||||||
return inviteStored, &util.JSONResponse{
|
return inviteStored, &util.JSONResponse{
|
||||||
Code: http.StatusBadRequest,
|
Code: http.StatusBadRequest,
|
||||||
JSON: jsonerror.BadJSON(err.Error()),
|
JSON: spec.BadJSON(err.Error()),
|
||||||
}
|
}
|
||||||
} else if err == threepid.ErrNotTrusted {
|
case threepid.ErrNotTrusted:
|
||||||
|
util.GetLogger(req.Context()).WithError(err).Error("threepid.CheckAndProcessInvite failed")
|
||||||
return inviteStored, &util.JSONResponse{
|
return inviteStored, &util.JSONResponse{
|
||||||
Code: http.StatusBadRequest,
|
Code: http.StatusBadRequest,
|
||||||
JSON: jsonerror.NotTrusted(body.IDServer),
|
JSON: spec.NotTrusted(body.IDServer),
|
||||||
}
|
}
|
||||||
} else if err == eventutil.ErrRoomNoExists {
|
case eventutil.ErrRoomNoExists:
|
||||||
|
util.GetLogger(req.Context()).WithError(err).Error("threepid.CheckAndProcessInvite failed")
|
||||||
return inviteStored, &util.JSONResponse{
|
return inviteStored, &util.JSONResponse{
|
||||||
Code: http.StatusNotFound,
|
Code: http.StatusNotFound,
|
||||||
JSON: jsonerror.NotFound(err.Error()),
|
JSON: spec.NotFound(err.Error()),
|
||||||
}
|
}
|
||||||
} else if e, ok := err.(gomatrixserverlib.BadJSONError); ok {
|
case gomatrixserverlib.BadJSONError:
|
||||||
|
util.GetLogger(req.Context()).WithError(err).Error("threepid.CheckAndProcessInvite failed")
|
||||||
return inviteStored, &util.JSONResponse{
|
return inviteStored, &util.JSONResponse{
|
||||||
Code: http.StatusBadRequest,
|
Code: http.StatusBadRequest,
|
||||||
JSON: jsonerror.BadJSON(e.Error()),
|
JSON: spec.BadJSON(e.Error()),
|
||||||
}
|
}
|
||||||
}
|
default:
|
||||||
if err != nil {
|
|
||||||
util.GetLogger(req.Context()).WithError(err).Error("threepid.CheckAndProcessInvite failed")
|
util.GetLogger(req.Context()).WithError(err).Error("threepid.CheckAndProcessInvite failed")
|
||||||
er := jsonerror.InternalServerError()
|
return inviteStored, &util.JSONResponse{
|
||||||
return inviteStored, &er
|
Code: http.StatusInternalServerError,
|
||||||
|
JSON: spec.InternalServerError{},
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
func checkMemberInRoom(ctx context.Context, rsAPI roomserverAPI.ClientRoomserverAPI, userID, roomID string) *util.JSONResponse {
|
func checkMemberInRoom(ctx context.Context, rsAPI roomserverAPI.ClientRoomserverAPI, userID spec.UserID, roomID string) *util.JSONResponse {
|
||||||
tuple := gomatrixserverlib.StateKeyTuple{
|
var membershipRes roomserverAPI.QueryMembershipForUserResponse
|
||||||
EventType: gomatrixserverlib.MRoomMember,
|
err := rsAPI.QueryMembershipForUser(ctx, &roomserverAPI.QueryMembershipForUserRequest{
|
||||||
StateKey: userID,
|
RoomID: roomID,
|
||||||
}
|
UserID: userID,
|
||||||
var membershipRes roomserverAPI.QueryCurrentStateResponse
|
|
||||||
err := rsAPI.QueryCurrentState(ctx, &roomserverAPI.QueryCurrentStateRequest{
|
|
||||||
RoomID: roomID,
|
|
||||||
StateTuples: []gomatrixserverlib.StateKeyTuple{tuple},
|
|
||||||
}, &membershipRes)
|
}, &membershipRes)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
util.GetLogger(ctx).WithError(err).Error("QueryCurrentState: could not query membership for user")
|
util.GetLogger(ctx).WithError(err).Error("QueryMembershipForUser: could not query membership for user")
|
||||||
e := jsonerror.InternalServerError()
|
|
||||||
return &e
|
|
||||||
}
|
|
||||||
ev := membershipRes.StateEvents[tuple]
|
|
||||||
if ev == nil {
|
|
||||||
return &util.JSONResponse{
|
return &util.JSONResponse{
|
||||||
Code: http.StatusForbidden,
|
Code: http.StatusInternalServerError,
|
||||||
JSON: jsonerror.Forbidden("user does not belong to room"),
|
JSON: spec.InternalServerError{},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
membership, err := ev.Membership()
|
if !membershipRes.IsInRoom {
|
||||||
if err != nil {
|
|
||||||
util.GetLogger(ctx).WithError(err).Error("Member event isn't valid")
|
|
||||||
e := jsonerror.InternalServerError()
|
|
||||||
return &e
|
|
||||||
}
|
|
||||||
if membership != gomatrixserverlib.Join {
|
|
||||||
return &util.JSONResponse{
|
return &util.JSONResponse{
|
||||||
Code: http.StatusForbidden,
|
Code: http.StatusForbidden,
|
||||||
JSON: jsonerror.Forbidden("user does not belong to room"),
|
JSON: spec.Forbidden("user does not belong to room"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
|
@ -474,26 +615,38 @@ func SendForget(
|
||||||
) util.JSONResponse {
|
) util.JSONResponse {
|
||||||
ctx := req.Context()
|
ctx := req.Context()
|
||||||
logger := util.GetLogger(ctx).WithField("roomID", roomID).WithField("userID", device.UserID)
|
logger := util.GetLogger(ctx).WithField("roomID", roomID).WithField("userID", device.UserID)
|
||||||
|
|
||||||
|
deviceUserID, err := spec.NewUserID(device.UserID, true)
|
||||||
|
if err != nil {
|
||||||
|
return util.JSONResponse{
|
||||||
|
Code: http.StatusForbidden,
|
||||||
|
JSON: spec.Forbidden("You don't have permission to kick this user, bad userID"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
var membershipRes roomserverAPI.QueryMembershipForUserResponse
|
var membershipRes roomserverAPI.QueryMembershipForUserResponse
|
||||||
membershipReq := roomserverAPI.QueryMembershipForUserRequest{
|
membershipReq := roomserverAPI.QueryMembershipForUserRequest{
|
||||||
RoomID: roomID,
|
RoomID: roomID,
|
||||||
UserID: device.UserID,
|
UserID: *deviceUserID,
|
||||||
}
|
}
|
||||||
err := rsAPI.QueryMembershipForUser(ctx, &membershipReq, &membershipRes)
|
err = rsAPI.QueryMembershipForUser(ctx, &membershipReq, &membershipRes)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.WithError(err).Error("QueryMembershipForUser: could not query membership for user")
|
logger.WithError(err).Error("QueryMembershipForUser: could not query membership for user")
|
||||||
return jsonerror.InternalServerError()
|
return util.JSONResponse{
|
||||||
|
Code: http.StatusInternalServerError,
|
||||||
|
JSON: spec.InternalServerError{},
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if !membershipRes.RoomExists {
|
if !membershipRes.RoomExists {
|
||||||
return util.JSONResponse{
|
return util.JSONResponse{
|
||||||
Code: http.StatusForbidden,
|
Code: http.StatusForbidden,
|
||||||
JSON: jsonerror.Forbidden("room does not exist"),
|
JSON: spec.Forbidden("room does not exist"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if membershipRes.IsInRoom {
|
if membershipRes.IsInRoom {
|
||||||
return util.JSONResponse{
|
return util.JSONResponse{
|
||||||
Code: http.StatusBadRequest,
|
Code: http.StatusBadRequest,
|
||||||
JSON: jsonerror.Unknown(fmt.Sprintf("User %s is in room %s", device.UserID, roomID)),
|
JSON: spec.Unknown(fmt.Sprintf("User %s is in room %s", device.UserID, roomID)),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -504,10 +657,34 @@ func SendForget(
|
||||||
response := roomserverAPI.PerformForgetResponse{}
|
response := roomserverAPI.PerformForgetResponse{}
|
||||||
if err := rsAPI.PerformForget(ctx, &request, &response); err != nil {
|
if err := rsAPI.PerformForget(ctx, &request, &response); err != nil {
|
||||||
logger.WithError(err).Error("PerformForget: unable to forget room")
|
logger.WithError(err).Error("PerformForget: unable to forget room")
|
||||||
return jsonerror.InternalServerError()
|
return util.JSONResponse{
|
||||||
|
Code: http.StatusInternalServerError,
|
||||||
|
JSON: spec.InternalServerError{},
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return util.JSONResponse{
|
return util.JSONResponse{
|
||||||
Code: http.StatusOK,
|
Code: http.StatusOK,
|
||||||
JSON: struct{}{},
|
JSON: struct{}{},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func getPowerlevels(req *http.Request, rsAPI roomserverAPI.ClientRoomserverAPI, roomID string) (*gomatrixserverlib.PowerLevelContent, *util.JSONResponse) {
|
||||||
|
plEvent := roomserverAPI.GetStateEvent(req.Context(), rsAPI, roomID, gomatrixserverlib.StateKeyTuple{
|
||||||
|
EventType: spec.MRoomPowerLevels,
|
||||||
|
StateKey: "",
|
||||||
|
})
|
||||||
|
if plEvent == nil {
|
||||||
|
return nil, &util.JSONResponse{
|
||||||
|
Code: http.StatusForbidden,
|
||||||
|
JSON: spec.Forbidden("You don't have permission to perform this action, no power_levels event in this room."),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pl, err := plEvent.PowerLevels()
|
||||||
|
if err != nil {
|
||||||
|
return nil, &util.JSONResponse{
|
||||||
|
Code: http.StatusForbidden,
|
||||||
|
JSON: spec.Forbidden("You don't have permission to perform this action, the power_levels event for this room is malformed so auth checks cannot be performed."),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return pl, nil
|
||||||
|
}
|
||||||
|
|
139
clientapi/routing/memberships.go
Normal file
139
clientapi/routing/memberships.go
Normal file
|
@ -0,0 +1,139 @@
|
||||||
|
// Copyright 2024 The Matrix.org Foundation C.I.C.
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
package routing
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/matrix-org/dendrite/roomserver/api"
|
||||||
|
userapi "github.com/matrix-org/dendrite/userapi/api"
|
||||||
|
"github.com/matrix-org/gomatrixserverlib/spec"
|
||||||
|
"github.com/matrix-org/util"
|
||||||
|
)
|
||||||
|
|
||||||
|
// https://matrix.org/docs/spec/client_server/r0.6.0#get-matrix-client-r0-rooms-roomid-joined-members
|
||||||
|
type getJoinedMembersResponse struct {
|
||||||
|
Joined map[string]joinedMember `json:"joined"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type joinedMember struct {
|
||||||
|
DisplayName string `json:"display_name"`
|
||||||
|
AvatarURL string `json:"avatar_url"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// The database stores 'displayname' without an underscore.
|
||||||
|
// Deserialize into this and then change to the actual API response
|
||||||
|
type databaseJoinedMember struct {
|
||||||
|
DisplayName string `json:"displayname"`
|
||||||
|
AvatarURL string `json:"avatar_url"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetJoinedMembers implements
|
||||||
|
//
|
||||||
|
// GET /rooms/{roomId}/joined_members
|
||||||
|
func GetJoinedMembers(
|
||||||
|
req *http.Request, device *userapi.Device, roomID string,
|
||||||
|
rsAPI api.ClientRoomserverAPI,
|
||||||
|
) util.JSONResponse {
|
||||||
|
// Validate the userID
|
||||||
|
userID, err := spec.NewUserID(device.UserID, true)
|
||||||
|
if err != nil {
|
||||||
|
return util.JSONResponse{
|
||||||
|
Code: http.StatusBadRequest,
|
||||||
|
JSON: spec.InvalidParam("Device UserID is invalid"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate the roomID
|
||||||
|
validRoomID, err := spec.NewRoomID(roomID)
|
||||||
|
if err != nil {
|
||||||
|
return util.JSONResponse{
|
||||||
|
Code: http.StatusBadRequest,
|
||||||
|
JSON: spec.InvalidParam("RoomID is invalid"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the current memberships for the requesting user to determine
|
||||||
|
// if they are allowed to query this endpoint.
|
||||||
|
queryReq := api.QueryMembershipForUserRequest{
|
||||||
|
RoomID: validRoomID.String(),
|
||||||
|
UserID: *userID,
|
||||||
|
}
|
||||||
|
|
||||||
|
var queryRes api.QueryMembershipForUserResponse
|
||||||
|
if queryErr := rsAPI.QueryMembershipForUser(req.Context(), &queryReq, &queryRes); queryErr != nil {
|
||||||
|
util.GetLogger(req.Context()).WithError(queryErr).Error("rsAPI.QueryMembershipsForRoom failed")
|
||||||
|
return util.JSONResponse{
|
||||||
|
Code: http.StatusInternalServerError,
|
||||||
|
JSON: spec.InternalServerError{},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !queryRes.HasBeenInRoom {
|
||||||
|
return util.JSONResponse{
|
||||||
|
Code: http.StatusForbidden,
|
||||||
|
JSON: spec.Forbidden("You aren't a member of the room and weren't previously a member of the room."),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !queryRes.IsInRoom {
|
||||||
|
return util.JSONResponse{
|
||||||
|
Code: http.StatusForbidden,
|
||||||
|
JSON: spec.Forbidden("You aren't a member of the room and weren't previously a member of the room."),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the current membership events
|
||||||
|
var membershipsForRoomResp api.QueryMembershipsForRoomResponse
|
||||||
|
if err = rsAPI.QueryMembershipsForRoom(req.Context(), &api.QueryMembershipsForRoomRequest{
|
||||||
|
JoinedOnly: true,
|
||||||
|
RoomID: validRoomID.String(),
|
||||||
|
}, &membershipsForRoomResp); err != nil {
|
||||||
|
util.GetLogger(req.Context()).WithError(err).Error("rsAPI.QueryEventsByID failed")
|
||||||
|
return util.JSONResponse{
|
||||||
|
Code: http.StatusInternalServerError,
|
||||||
|
JSON: spec.InternalServerError{},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var res getJoinedMembersResponse
|
||||||
|
res.Joined = make(map[string]joinedMember)
|
||||||
|
for _, ev := range membershipsForRoomResp.JoinEvents {
|
||||||
|
var content databaseJoinedMember
|
||||||
|
if err := json.Unmarshal(ev.Content, &content); err != nil {
|
||||||
|
util.GetLogger(req.Context()).WithError(err).Error("failed to unmarshal event content")
|
||||||
|
return util.JSONResponse{
|
||||||
|
Code: http.StatusInternalServerError,
|
||||||
|
JSON: spec.InternalServerError{},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
userID, err := rsAPI.QueryUserIDForSender(req.Context(), *validRoomID, spec.SenderID(ev.Sender))
|
||||||
|
if err != nil || userID == nil {
|
||||||
|
util.GetLogger(req.Context()).WithError(err).Error("rsAPI.QueryUserIDForSender failed")
|
||||||
|
return util.JSONResponse{
|
||||||
|
Code: http.StatusInternalServerError,
|
||||||
|
JSON: spec.InternalServerError{},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
res.Joined[userID.String()] = joinedMember(content)
|
||||||
|
}
|
||||||
|
return util.JSONResponse{
|
||||||
|
Code: http.StatusOK,
|
||||||
|
JSON: res,
|
||||||
|
}
|
||||||
|
}
|
|
@ -18,9 +18,9 @@ import (
|
||||||
"net/http"
|
"net/http"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
|
||||||
"github.com/matrix-org/dendrite/clientapi/jsonerror"
|
|
||||||
userapi "github.com/matrix-org/dendrite/userapi/api"
|
userapi "github.com/matrix-org/dendrite/userapi/api"
|
||||||
"github.com/matrix-org/gomatrixserverlib"
|
"github.com/matrix-org/gomatrixserverlib"
|
||||||
|
"github.com/matrix-org/gomatrixserverlib/spec"
|
||||||
"github.com/matrix-org/util"
|
"github.com/matrix-org/util"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -35,7 +35,10 @@ func GetNotifications(
|
||||||
limit, err = strconv.ParseInt(limitStr, 10, 64)
|
limit, err = strconv.ParseInt(limitStr, 10, 64)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
util.GetLogger(req.Context()).WithError(err).Error("ParseInt(limit) failed")
|
util.GetLogger(req.Context()).WithError(err).Error("ParseInt(limit) failed")
|
||||||
return jsonerror.InternalServerError()
|
return util.JSONResponse{
|
||||||
|
Code: http.StatusInternalServerError,
|
||||||
|
JSON: spec.InternalServerError{},
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -43,7 +46,10 @@ func GetNotifications(
|
||||||
localpart, domain, err := gomatrixserverlib.SplitID('@', device.UserID)
|
localpart, domain, err := gomatrixserverlib.SplitID('@', device.UserID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
util.GetLogger(req.Context()).WithError(err).Error("SplitID failed")
|
util.GetLogger(req.Context()).WithError(err).Error("SplitID failed")
|
||||||
return jsonerror.InternalServerError()
|
return util.JSONResponse{
|
||||||
|
Code: http.StatusInternalServerError,
|
||||||
|
JSON: spec.InternalServerError{},
|
||||||
|
}
|
||||||
}
|
}
|
||||||
err = userAPI.QueryNotifications(req.Context(), &userapi.QueryNotificationsRequest{
|
err = userAPI.QueryNotifications(req.Context(), &userapi.QueryNotificationsRequest{
|
||||||
Localpart: localpart,
|
Localpart: localpart,
|
||||||
|
@ -54,7 +60,10 @@ func GetNotifications(
|
||||||
}, &queryRes)
|
}, &queryRes)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
util.GetLogger(req.Context()).WithError(err).Error("QueryNotifications failed")
|
util.GetLogger(req.Context()).WithError(err).Error("QueryNotifications failed")
|
||||||
return jsonerror.InternalServerError()
|
return util.JSONResponse{
|
||||||
|
Code: http.StatusInternalServerError,
|
||||||
|
JSON: spec.InternalServerError{},
|
||||||
|
}
|
||||||
}
|
}
|
||||||
util.GetLogger(req.Context()).WithField("from", req.URL.Query().Get("from")).WithField("limit", limit).WithField("only", req.URL.Query().Get("only")).WithField("next", queryRes.NextToken).Infof("QueryNotifications: len %d", len(queryRes.Notifications))
|
util.GetLogger(req.Context()).WithField("from", req.URL.Query().Get("from")).WithField("limit", limit).WithField("only", req.URL.Query().Get("only")).WithField("next", queryRes.NextToken).Infof("QueryNotifications: len %d", len(queryRes.Notifications))
|
||||||
return util.JSONResponse{
|
return util.JSONResponse{
|
||||||
|
|
|
@ -17,9 +17,9 @@ package routing
|
||||||
import (
|
import (
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
"github.com/matrix-org/dendrite/clientapi/jsonerror"
|
|
||||||
"github.com/matrix-org/dendrite/setup/config"
|
"github.com/matrix-org/dendrite/setup/config"
|
||||||
"github.com/matrix-org/dendrite/userapi/api"
|
"github.com/matrix-org/dendrite/userapi/api"
|
||||||
|
"github.com/matrix-org/gomatrixserverlib/spec"
|
||||||
"github.com/matrix-org/util"
|
"github.com/matrix-org/util"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -43,7 +43,7 @@ func CreateOpenIDToken(
|
||||||
if userID != device.UserID {
|
if userID != device.UserID {
|
||||||
return util.JSONResponse{
|
return util.JSONResponse{
|
||||||
Code: http.StatusForbidden,
|
Code: http.StatusForbidden,
|
||||||
JSON: jsonerror.Forbidden("Cannot request tokens for other users"),
|
JSON: spec.Forbidden("Cannot request tokens for other users"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -55,7 +55,10 @@ func CreateOpenIDToken(
|
||||||
err := userAPI.PerformOpenIDTokenCreation(req.Context(), &request, &response)
|
err := userAPI.PerformOpenIDTokenCreation(req.Context(), &request, &response)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
util.GetLogger(req.Context()).WithError(err).Error("userAPI.CreateOpenIDToken failed")
|
util.GetLogger(req.Context()).WithError(err).Error("userAPI.CreateOpenIDToken failed")
|
||||||
return jsonerror.InternalServerError()
|
return util.JSONResponse{
|
||||||
|
Code: http.StatusInternalServerError,
|
||||||
|
JSON: spec.InternalServerError{},
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return util.JSONResponse{
|
return util.JSONResponse{
|
||||||
|
|
|
@ -6,11 +6,11 @@ import (
|
||||||
"github.com/matrix-org/dendrite/clientapi/auth"
|
"github.com/matrix-org/dendrite/clientapi/auth"
|
||||||
"github.com/matrix-org/dendrite/clientapi/auth/authtypes"
|
"github.com/matrix-org/dendrite/clientapi/auth/authtypes"
|
||||||
"github.com/matrix-org/dendrite/clientapi/httputil"
|
"github.com/matrix-org/dendrite/clientapi/httputil"
|
||||||
"github.com/matrix-org/dendrite/clientapi/jsonerror"
|
|
||||||
"github.com/matrix-org/dendrite/internal"
|
"github.com/matrix-org/dendrite/internal"
|
||||||
"github.com/matrix-org/dendrite/setup/config"
|
"github.com/matrix-org/dendrite/setup/config"
|
||||||
"github.com/matrix-org/dendrite/userapi/api"
|
"github.com/matrix-org/dendrite/userapi/api"
|
||||||
"github.com/matrix-org/gomatrixserverlib"
|
"github.com/matrix-org/gomatrixserverlib"
|
||||||
|
"github.com/matrix-org/gomatrixserverlib/spec"
|
||||||
"github.com/matrix-org/util"
|
"github.com/matrix-org/util"
|
||||||
"github.com/sirupsen/logrus"
|
"github.com/sirupsen/logrus"
|
||||||
)
|
)
|
||||||
|
@ -73,8 +73,8 @@ func Password(
|
||||||
|
|
||||||
// Check if the existing password is correct.
|
// Check if the existing password is correct.
|
||||||
typePassword := auth.LoginTypePassword{
|
typePassword := auth.LoginTypePassword{
|
||||||
GetAccountByPassword: userAPI.QueryAccountByPassword,
|
UserAPI: userAPI,
|
||||||
Config: cfg,
|
Config: cfg,
|
||||||
}
|
}
|
||||||
if _, authErr := typePassword.Login(req.Context(), &r.Auth.PasswordRequest); authErr != nil {
|
if _, authErr := typePassword.Login(req.Context(), &r.Auth.PasswordRequest); authErr != nil {
|
||||||
return *authErr
|
return *authErr
|
||||||
|
@ -90,7 +90,10 @@ func Password(
|
||||||
localpart, domain, err := gomatrixserverlib.SplitID('@', device.UserID)
|
localpart, domain, err := gomatrixserverlib.SplitID('@', device.UserID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
util.GetLogger(req.Context()).WithError(err).Error("gomatrixserverlib.SplitID failed")
|
util.GetLogger(req.Context()).WithError(err).Error("gomatrixserverlib.SplitID failed")
|
||||||
return jsonerror.InternalServerError()
|
return util.JSONResponse{
|
||||||
|
Code: http.StatusInternalServerError,
|
||||||
|
JSON: spec.InternalServerError{},
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Ask the user API to perform the password change.
|
// Ask the user API to perform the password change.
|
||||||
|
@ -102,11 +105,17 @@ func Password(
|
||||||
passwordRes := &api.PerformPasswordUpdateResponse{}
|
passwordRes := &api.PerformPasswordUpdateResponse{}
|
||||||
if err := userAPI.PerformPasswordUpdate(req.Context(), passwordReq, passwordRes); err != nil {
|
if err := userAPI.PerformPasswordUpdate(req.Context(), passwordReq, passwordRes); err != nil {
|
||||||
util.GetLogger(req.Context()).WithError(err).Error("PerformPasswordUpdate failed")
|
util.GetLogger(req.Context()).WithError(err).Error("PerformPasswordUpdate failed")
|
||||||
return jsonerror.InternalServerError()
|
return util.JSONResponse{
|
||||||
|
Code: http.StatusInternalServerError,
|
||||||
|
JSON: spec.InternalServerError{},
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if !passwordRes.PasswordUpdated {
|
if !passwordRes.PasswordUpdated {
|
||||||
util.GetLogger(req.Context()).Error("Expected password to have been updated but wasn't")
|
util.GetLogger(req.Context()).Error("Expected password to have been updated but wasn't")
|
||||||
return jsonerror.InternalServerError()
|
return util.JSONResponse{
|
||||||
|
Code: http.StatusInternalServerError,
|
||||||
|
JSON: spec.InternalServerError{},
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// If the request asks us to log out all other devices then
|
// If the request asks us to log out all other devices then
|
||||||
|
@ -120,7 +129,10 @@ func Password(
|
||||||
logoutRes := &api.PerformDeviceDeletionResponse{}
|
logoutRes := &api.PerformDeviceDeletionResponse{}
|
||||||
if err := userAPI.PerformDeviceDeletion(req.Context(), logoutReq, logoutRes); err != nil {
|
if err := userAPI.PerformDeviceDeletion(req.Context(), logoutReq, logoutRes); err != nil {
|
||||||
util.GetLogger(req.Context()).WithError(err).Error("PerformDeviceDeletion failed")
|
util.GetLogger(req.Context()).WithError(err).Error("PerformDeviceDeletion failed")
|
||||||
return jsonerror.InternalServerError()
|
return util.JSONResponse{
|
||||||
|
Code: http.StatusInternalServerError,
|
||||||
|
JSON: spec.InternalServerError{},
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pushersReq := &api.PerformPusherDeletionRequest{
|
pushersReq := &api.PerformPusherDeletionRequest{
|
||||||
|
@ -130,7 +142,10 @@ func Password(
|
||||||
}
|
}
|
||||||
if err := userAPI.PerformPusherDeletion(req.Context(), pushersReq, &struct{}{}); err != nil {
|
if err := userAPI.PerformPusherDeletion(req.Context(), pushersReq, &struct{}{}); err != nil {
|
||||||
util.GetLogger(req.Context()).WithError(err).Error("PerformPusherDeletion failed")
|
util.GetLogger(req.Context()).WithError(err).Error("PerformPusherDeletion failed")
|
||||||
return jsonerror.InternalServerError()
|
return util.JSONResponse{
|
||||||
|
Code: http.StatusInternalServerError,
|
||||||
|
JSON: spec.InternalServerError{},
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -15,13 +15,15 @@
|
||||||
package routing
|
package routing
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"encoding/json"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
"github.com/matrix-org/dendrite/clientapi/jsonerror"
|
|
||||||
roomserverAPI "github.com/matrix-org/dendrite/roomserver/api"
|
roomserverAPI "github.com/matrix-org/dendrite/roomserver/api"
|
||||||
"github.com/matrix-org/dendrite/userapi/api"
|
"github.com/matrix-org/dendrite/userapi/api"
|
||||||
"github.com/matrix-org/gomatrixserverlib"
|
"github.com/matrix-org/gomatrix"
|
||||||
|
"github.com/matrix-org/gomatrixserverlib/spec"
|
||||||
"github.com/matrix-org/util"
|
"github.com/matrix-org/util"
|
||||||
|
"github.com/sirupsen/logrus"
|
||||||
)
|
)
|
||||||
|
|
||||||
func PeekRoomByIDOrAlias(
|
func PeekRoomByIDOrAlias(
|
||||||
|
@ -41,25 +43,42 @@ func PeekRoomByIDOrAlias(
|
||||||
UserID: device.UserID,
|
UserID: device.UserID,
|
||||||
DeviceID: device.ID,
|
DeviceID: device.ID,
|
||||||
}
|
}
|
||||||
peekRes := roomserverAPI.PerformPeekResponse{}
|
|
||||||
|
|
||||||
// Check to see if any ?server_name= query parameters were
|
// Check to see if any ?server_name= query parameters were
|
||||||
// given in the request.
|
// given in the request.
|
||||||
if serverNames, ok := req.URL.Query()["server_name"]; ok {
|
if serverNames, ok := req.URL.Query()["server_name"]; ok {
|
||||||
for _, serverName := range serverNames {
|
for _, serverName := range serverNames {
|
||||||
peekReq.ServerNames = append(
|
peekReq.ServerNames = append(
|
||||||
peekReq.ServerNames,
|
peekReq.ServerNames,
|
||||||
gomatrixserverlib.ServerName(serverName),
|
spec.ServerName(serverName),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Ask the roomserver to perform the peek.
|
// Ask the roomserver to perform the peek.
|
||||||
if err := rsAPI.PerformPeek(req.Context(), &peekReq, &peekRes); err != nil {
|
roomID, err := rsAPI.PerformPeek(req.Context(), &peekReq)
|
||||||
return util.ErrorResponse(err)
|
switch e := err.(type) {
|
||||||
}
|
case roomserverAPI.ErrInvalidID:
|
||||||
if peekRes.Error != nil {
|
return util.JSONResponse{
|
||||||
return peekRes.Error.JSONResponse()
|
Code: http.StatusBadRequest,
|
||||||
|
JSON: spec.Unknown(e.Error()),
|
||||||
|
}
|
||||||
|
case roomserverAPI.ErrNotAllowed:
|
||||||
|
return util.JSONResponse{
|
||||||
|
Code: http.StatusForbidden,
|
||||||
|
JSON: spec.Forbidden(e.Error()),
|
||||||
|
}
|
||||||
|
case *gomatrix.HTTPError:
|
||||||
|
return util.JSONResponse{
|
||||||
|
Code: e.Code,
|
||||||
|
JSON: json.RawMessage(e.Message),
|
||||||
|
}
|
||||||
|
case nil:
|
||||||
|
default:
|
||||||
|
logrus.WithError(err).WithField("roomID", roomIDOrAlias).Errorf("Failed to peek room")
|
||||||
|
return util.JSONResponse{
|
||||||
|
Code: http.StatusInternalServerError,
|
||||||
|
JSON: spec.InternalServerError{},
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// if this user is already joined to the room, we let them peek anyway
|
// if this user is already joined to the room, we let them peek anyway
|
||||||
|
@ -75,7 +94,7 @@ func PeekRoomByIDOrAlias(
|
||||||
// TODO: Put the response struct somewhere internal.
|
// TODO: Put the response struct somewhere internal.
|
||||||
JSON: struct {
|
JSON: struct {
|
||||||
RoomID string `json:"room_id"`
|
RoomID string `json:"room_id"`
|
||||||
}{peekRes.RoomID},
|
}{roomID},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -85,18 +104,20 @@ func UnpeekRoomByID(
|
||||||
rsAPI roomserverAPI.ClientRoomserverAPI,
|
rsAPI roomserverAPI.ClientRoomserverAPI,
|
||||||
roomID string,
|
roomID string,
|
||||||
) util.JSONResponse {
|
) util.JSONResponse {
|
||||||
unpeekReq := roomserverAPI.PerformUnpeekRequest{
|
err := rsAPI.PerformUnpeek(req.Context(), roomID, device.UserID, device.ID)
|
||||||
RoomID: roomID,
|
switch e := err.(type) {
|
||||||
UserID: device.UserID,
|
case roomserverAPI.ErrInvalidID:
|
||||||
DeviceID: device.ID,
|
return util.JSONResponse{
|
||||||
}
|
Code: http.StatusBadRequest,
|
||||||
unpeekRes := roomserverAPI.PerformUnpeekResponse{}
|
JSON: spec.Unknown(e.Error()),
|
||||||
|
}
|
||||||
if err := rsAPI.PerformUnpeek(req.Context(), &unpeekReq, &unpeekRes); err != nil {
|
case nil:
|
||||||
return jsonerror.InternalAPIError(req.Context(), err)
|
default:
|
||||||
}
|
logrus.WithError(err).WithField("roomID", roomID).Errorf("Failed to un-peek room")
|
||||||
if unpeekRes.Error != nil {
|
return util.JSONResponse{
|
||||||
return unpeekRes.Error.JSONResponse()
|
Code: http.StatusInternalServerError,
|
||||||
|
JSON: spec.InternalServerError{},
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return util.JSONResponse{
|
return util.JSONResponse{
|
||||||
|
|
|
@ -21,13 +21,12 @@ import (
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/matrix-org/dendrite/clientapi/httputil"
|
"github.com/matrix-org/dendrite/clientapi/httputil"
|
||||||
"github.com/matrix-org/dendrite/clientapi/jsonerror"
|
|
||||||
"github.com/matrix-org/dendrite/clientapi/producers"
|
"github.com/matrix-org/dendrite/clientapi/producers"
|
||||||
"github.com/matrix-org/dendrite/setup/config"
|
"github.com/matrix-org/dendrite/setup/config"
|
||||||
"github.com/matrix-org/dendrite/setup/jetstream"
|
"github.com/matrix-org/dendrite/setup/jetstream"
|
||||||
"github.com/matrix-org/dendrite/syncapi/types"
|
"github.com/matrix-org/dendrite/syncapi/types"
|
||||||
"github.com/matrix-org/dendrite/userapi/api"
|
"github.com/matrix-org/dendrite/userapi/api"
|
||||||
"github.com/matrix-org/gomatrixserverlib"
|
"github.com/matrix-org/gomatrixserverlib/spec"
|
||||||
"github.com/matrix-org/util"
|
"github.com/matrix-org/util"
|
||||||
"github.com/nats-io/nats.go"
|
"github.com/nats-io/nats.go"
|
||||||
log "github.com/sirupsen/logrus"
|
log "github.com/sirupsen/logrus"
|
||||||
|
@ -54,7 +53,7 @@ func SetPresence(
|
||||||
if device.UserID != userID {
|
if device.UserID != userID {
|
||||||
return util.JSONResponse{
|
return util.JSONResponse{
|
||||||
Code: http.StatusForbidden,
|
Code: http.StatusForbidden,
|
||||||
JSON: jsonerror.Forbidden("Unable to set presence for other user."),
|
JSON: spec.Forbidden("Unable to set presence for other user."),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
var presence presenceReq
|
var presence presenceReq
|
||||||
|
@ -67,7 +66,7 @@ func SetPresence(
|
||||||
if !ok {
|
if !ok {
|
||||||
return util.JSONResponse{
|
return util.JSONResponse{
|
||||||
Code: http.StatusBadRequest,
|
Code: http.StatusBadRequest,
|
||||||
JSON: jsonerror.Unknown(fmt.Sprintf("Unknown presence '%s'.", presence.Presence)),
|
JSON: spec.Unknown(fmt.Sprintf("Unknown presence '%s'.", presence.Presence)),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
err := producer.SendPresence(req.Context(), userID, presenceStatus, presence.StatusMsg)
|
err := producer.SendPresence(req.Context(), userID, presenceStatus, presence.StatusMsg)
|
||||||
|
@ -75,7 +74,7 @@ func SetPresence(
|
||||||
log.WithError(err).Errorf("failed to update presence")
|
log.WithError(err).Errorf("failed to update presence")
|
||||||
return util.JSONResponse{
|
return util.JSONResponse{
|
||||||
Code: http.StatusInternalServerError,
|
Code: http.StatusInternalServerError,
|
||||||
JSON: jsonerror.InternalServerError(),
|
JSON: spec.InternalServerError{},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -100,7 +99,7 @@ func GetPresence(
|
||||||
log.WithError(err).Errorf("unable to get presence")
|
log.WithError(err).Errorf("unable to get presence")
|
||||||
return util.JSONResponse{
|
return util.JSONResponse{
|
||||||
Code: http.StatusInternalServerError,
|
Code: http.StatusInternalServerError,
|
||||||
JSON: jsonerror.InternalServerError(),
|
JSON: spec.InternalServerError{},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -119,11 +118,11 @@ func GetPresence(
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return util.JSONResponse{
|
return util.JSONResponse{
|
||||||
Code: http.StatusInternalServerError,
|
Code: http.StatusInternalServerError,
|
||||||
JSON: jsonerror.InternalServerError(),
|
JSON: spec.InternalServerError{},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
p := types.PresenceInternal{LastActiveTS: gomatrixserverlib.Timestamp(lastActive)}
|
p := types.PresenceInternal{LastActiveTS: spec.Timestamp(lastActive)}
|
||||||
currentlyActive := p.CurrentlyActive()
|
currentlyActive := p.CurrentlyActive()
|
||||||
return util.JSONResponse{
|
return util.JSONResponse{
|
||||||
Code: http.StatusOK,
|
Code: http.StatusOK,
|
||||||
|
|
|
@ -16,47 +16,52 @@ package routing
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/matrix-org/gomatrixserverlib"
|
"github.com/matrix-org/gomatrixserverlib"
|
||||||
|
"github.com/matrix-org/gomatrixserverlib/fclient"
|
||||||
|
"github.com/matrix-org/gomatrixserverlib/spec"
|
||||||
|
|
||||||
appserviceAPI "github.com/matrix-org/dendrite/appservice/api"
|
appserviceAPI "github.com/matrix-org/dendrite/appservice/api"
|
||||||
"github.com/matrix-org/dendrite/clientapi/auth/authtypes"
|
"github.com/matrix-org/dendrite/clientapi/auth/authtypes"
|
||||||
"github.com/matrix-org/dendrite/clientapi/httputil"
|
"github.com/matrix-org/dendrite/clientapi/httputil"
|
||||||
"github.com/matrix-org/dendrite/clientapi/jsonerror"
|
|
||||||
"github.com/matrix-org/dendrite/internal/eventutil"
|
"github.com/matrix-org/dendrite/internal/eventutil"
|
||||||
"github.com/matrix-org/dendrite/roomserver/api"
|
"github.com/matrix-org/dendrite/roomserver/api"
|
||||||
|
"github.com/matrix-org/dendrite/roomserver/types"
|
||||||
"github.com/matrix-org/dendrite/setup/config"
|
"github.com/matrix-org/dendrite/setup/config"
|
||||||
userapi "github.com/matrix-org/dendrite/userapi/api"
|
userapi "github.com/matrix-org/dendrite/userapi/api"
|
||||||
|
|
||||||
"github.com/matrix-org/gomatrix"
|
"github.com/matrix-org/gomatrix"
|
||||||
"github.com/matrix-org/util"
|
"github.com/matrix-org/util"
|
||||||
)
|
)
|
||||||
|
|
||||||
// GetProfile implements GET /profile/{userID}
|
// GetProfile implements GET /profile/{userID}
|
||||||
func GetProfile(
|
func GetProfile(
|
||||||
req *http.Request, profileAPI userapi.ClientUserAPI, cfg *config.ClientAPI,
|
req *http.Request, profileAPI userapi.ProfileAPI, cfg *config.ClientAPI,
|
||||||
userID string,
|
userID string,
|
||||||
asAPI appserviceAPI.AppServiceInternalAPI,
|
asAPI appserviceAPI.AppServiceInternalAPI,
|
||||||
federation *gomatrixserverlib.FederationClient,
|
federation fclient.FederationClient,
|
||||||
) util.JSONResponse {
|
) util.JSONResponse {
|
||||||
profile, err := getProfile(req.Context(), profileAPI, cfg, userID, asAPI, federation)
|
profile, err := getProfile(req.Context(), profileAPI, cfg, userID, asAPI, federation)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if err == eventutil.ErrProfileNoExists {
|
if err == appserviceAPI.ErrProfileNotExists {
|
||||||
return util.JSONResponse{
|
return util.JSONResponse{
|
||||||
Code: http.StatusNotFound,
|
Code: http.StatusNotFound,
|
||||||
JSON: jsonerror.NotFound("The user does not exist or does not have a profile"),
|
JSON: spec.NotFound("The user does not exist or does not have a profile"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
util.GetLogger(req.Context()).WithError(err).Error("getProfile failed")
|
util.GetLogger(req.Context()).WithError(err).Error("getProfile failed")
|
||||||
return jsonerror.InternalServerError()
|
return util.JSONResponse{
|
||||||
|
Code: http.StatusInternalServerError,
|
||||||
|
JSON: spec.InternalServerError{},
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return util.JSONResponse{
|
return util.JSONResponse{
|
||||||
Code: http.StatusOK,
|
Code: http.StatusOK,
|
||||||
JSON: eventutil.ProfileResponse{
|
JSON: eventutil.UserProfile{
|
||||||
AvatarURL: profile.AvatarURL,
|
AvatarURL: profile.AvatarURL,
|
||||||
DisplayName: profile.DisplayName,
|
DisplayName: profile.DisplayName,
|
||||||
},
|
},
|
||||||
|
@ -65,64 +70,55 @@ func GetProfile(
|
||||||
|
|
||||||
// GetAvatarURL implements GET /profile/{userID}/avatar_url
|
// GetAvatarURL implements GET /profile/{userID}/avatar_url
|
||||||
func GetAvatarURL(
|
func GetAvatarURL(
|
||||||
req *http.Request, profileAPI userapi.ClientUserAPI, cfg *config.ClientAPI,
|
req *http.Request, profileAPI userapi.ProfileAPI, cfg *config.ClientAPI,
|
||||||
userID string, asAPI appserviceAPI.AppServiceInternalAPI,
|
userID string, asAPI appserviceAPI.AppServiceInternalAPI,
|
||||||
federation *gomatrixserverlib.FederationClient,
|
federation fclient.FederationClient,
|
||||||
) util.JSONResponse {
|
) util.JSONResponse {
|
||||||
profile, err := getProfile(req.Context(), profileAPI, cfg, userID, asAPI, federation)
|
profile := GetProfile(req, profileAPI, cfg, userID, asAPI, federation)
|
||||||
if err != nil {
|
p, ok := profile.JSON.(eventutil.UserProfile)
|
||||||
if err == eventutil.ErrProfileNoExists {
|
// not a profile response, so most likely an error, return that
|
||||||
return util.JSONResponse{
|
if !ok {
|
||||||
Code: http.StatusNotFound,
|
return profile
|
||||||
JSON: jsonerror.NotFound("The user does not exist or does not have a profile"),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
util.GetLogger(req.Context()).WithError(err).Error("getProfile failed")
|
|
||||||
return jsonerror.InternalServerError()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return util.JSONResponse{
|
return util.JSONResponse{
|
||||||
Code: http.StatusOK,
|
Code: http.StatusOK,
|
||||||
JSON: eventutil.AvatarURL{
|
JSON: eventutil.UserProfile{
|
||||||
AvatarURL: profile.AvatarURL,
|
AvatarURL: p.AvatarURL,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// SetAvatarURL implements PUT /profile/{userID}/avatar_url
|
// SetAvatarURL implements PUT /profile/{userID}/avatar_url
|
||||||
func SetAvatarURL(
|
func SetAvatarURL(
|
||||||
req *http.Request, profileAPI userapi.ClientUserAPI,
|
req *http.Request, profileAPI userapi.ProfileAPI,
|
||||||
device *userapi.Device, userID string, cfg *config.ClientAPI, rsAPI api.ClientRoomserverAPI,
|
device *userapi.Device, userID string, cfg *config.ClientAPI, rsAPI api.ClientRoomserverAPI,
|
||||||
) util.JSONResponse {
|
) util.JSONResponse {
|
||||||
if userID != device.UserID {
|
if userID != device.UserID {
|
||||||
return util.JSONResponse{
|
return util.JSONResponse{
|
||||||
Code: http.StatusForbidden,
|
Code: http.StatusForbidden,
|
||||||
JSON: jsonerror.Forbidden("userID does not match the current user"),
|
JSON: spec.Forbidden("userID does not match the current user"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var r eventutil.AvatarURL
|
var r eventutil.UserProfile
|
||||||
if resErr := httputil.UnmarshalJSONRequest(req, &r); resErr != nil {
|
if resErr := httputil.UnmarshalJSONRequest(req, &r); resErr != nil {
|
||||||
return *resErr
|
return *resErr
|
||||||
}
|
}
|
||||||
if r.AvatarURL == "" {
|
|
||||||
return util.JSONResponse{
|
|
||||||
Code: http.StatusBadRequest,
|
|
||||||
JSON: jsonerror.BadJSON("'avatar_url' must be supplied."),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
localpart, domain, err := gomatrixserverlib.SplitID('@', userID)
|
localpart, domain, err := gomatrixserverlib.SplitID('@', userID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
util.GetLogger(req.Context()).WithError(err).Error("gomatrixserverlib.SplitID failed")
|
util.GetLogger(req.Context()).WithError(err).Error("gomatrixserverlib.SplitID failed")
|
||||||
return jsonerror.InternalServerError()
|
return util.JSONResponse{
|
||||||
|
Code: http.StatusInternalServerError,
|
||||||
|
JSON: spec.InternalServerError{},
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if !cfg.Matrix.IsLocalServerName(domain) {
|
if !cfg.Matrix.IsLocalServerName(domain) {
|
||||||
return util.JSONResponse{
|
return util.JSONResponse{
|
||||||
Code: http.StatusForbidden,
|
Code: http.StatusForbidden,
|
||||||
JSON: jsonerror.Forbidden("userID does not belong to a locally configured domain"),
|
JSON: spec.Forbidden("userID does not belong to a locally configured domain"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -130,28 +126,27 @@ func SetAvatarURL(
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return util.JSONResponse{
|
return util.JSONResponse{
|
||||||
Code: http.StatusBadRequest,
|
Code: http.StatusBadRequest,
|
||||||
JSON: jsonerror.InvalidArgumentValue(err.Error()),
|
JSON: spec.InvalidParam(err.Error()),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
setRes := &userapi.PerformSetAvatarURLResponse{}
|
profile, changed, err := profileAPI.SetAvatarURL(req.Context(), localpart, domain, r.AvatarURL)
|
||||||
if err = profileAPI.SetAvatarURL(req.Context(), &userapi.PerformSetAvatarURLRequest{
|
if err != nil {
|
||||||
Localpart: localpart,
|
|
||||||
ServerName: domain,
|
|
||||||
AvatarURL: r.AvatarURL,
|
|
||||||
}, setRes); err != nil {
|
|
||||||
util.GetLogger(req.Context()).WithError(err).Error("profileAPI.SetAvatarURL failed")
|
util.GetLogger(req.Context()).WithError(err).Error("profileAPI.SetAvatarURL failed")
|
||||||
return jsonerror.InternalServerError()
|
return util.JSONResponse{
|
||||||
|
Code: http.StatusInternalServerError,
|
||||||
|
JSON: spec.InternalServerError{},
|
||||||
|
}
|
||||||
}
|
}
|
||||||
// No need to build new membership events, since nothing changed
|
// No need to build new membership events, since nothing changed
|
||||||
if !setRes.Changed {
|
if !changed {
|
||||||
return util.JSONResponse{
|
return util.JSONResponse{
|
||||||
Code: http.StatusOK,
|
Code: http.StatusOK,
|
||||||
JSON: struct{}{},
|
JSON: struct{}{},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
response, err := updateProfile(req.Context(), rsAPI, device, setRes.Profile, userID, cfg, evTime)
|
response, err := updateProfile(req.Context(), rsAPI, device, profile, userID, evTime)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return response
|
return response
|
||||||
}
|
}
|
||||||
|
@ -164,64 +159,55 @@ func SetAvatarURL(
|
||||||
|
|
||||||
// GetDisplayName implements GET /profile/{userID}/displayname
|
// GetDisplayName implements GET /profile/{userID}/displayname
|
||||||
func GetDisplayName(
|
func GetDisplayName(
|
||||||
req *http.Request, profileAPI userapi.ClientUserAPI, cfg *config.ClientAPI,
|
req *http.Request, profileAPI userapi.ProfileAPI, cfg *config.ClientAPI,
|
||||||
userID string, asAPI appserviceAPI.AppServiceInternalAPI,
|
userID string, asAPI appserviceAPI.AppServiceInternalAPI,
|
||||||
federation *gomatrixserverlib.FederationClient,
|
federation fclient.FederationClient,
|
||||||
) util.JSONResponse {
|
) util.JSONResponse {
|
||||||
profile, err := getProfile(req.Context(), profileAPI, cfg, userID, asAPI, federation)
|
profile := GetProfile(req, profileAPI, cfg, userID, asAPI, federation)
|
||||||
if err != nil {
|
p, ok := profile.JSON.(eventutil.UserProfile)
|
||||||
if err == eventutil.ErrProfileNoExists {
|
// not a profile response, so most likely an error, return that
|
||||||
return util.JSONResponse{
|
if !ok {
|
||||||
Code: http.StatusNotFound,
|
return profile
|
||||||
JSON: jsonerror.NotFound("The user does not exist or does not have a profile"),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
util.GetLogger(req.Context()).WithError(err).Error("getProfile failed")
|
|
||||||
return jsonerror.InternalServerError()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return util.JSONResponse{
|
return util.JSONResponse{
|
||||||
Code: http.StatusOK,
|
Code: http.StatusOK,
|
||||||
JSON: eventutil.DisplayName{
|
JSON: eventutil.UserProfile{
|
||||||
DisplayName: profile.DisplayName,
|
DisplayName: p.DisplayName,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// SetDisplayName implements PUT /profile/{userID}/displayname
|
// SetDisplayName implements PUT /profile/{userID}/displayname
|
||||||
func SetDisplayName(
|
func SetDisplayName(
|
||||||
req *http.Request, profileAPI userapi.ClientUserAPI,
|
req *http.Request, profileAPI userapi.ProfileAPI,
|
||||||
device *userapi.Device, userID string, cfg *config.ClientAPI, rsAPI api.ClientRoomserverAPI,
|
device *userapi.Device, userID string, cfg *config.ClientAPI, rsAPI api.ClientRoomserverAPI,
|
||||||
) util.JSONResponse {
|
) util.JSONResponse {
|
||||||
if userID != device.UserID {
|
if userID != device.UserID {
|
||||||
return util.JSONResponse{
|
return util.JSONResponse{
|
||||||
Code: http.StatusForbidden,
|
Code: http.StatusForbidden,
|
||||||
JSON: jsonerror.Forbidden("userID does not match the current user"),
|
JSON: spec.Forbidden("userID does not match the current user"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var r eventutil.DisplayName
|
var r eventutil.UserProfile
|
||||||
if resErr := httputil.UnmarshalJSONRequest(req, &r); resErr != nil {
|
if resErr := httputil.UnmarshalJSONRequest(req, &r); resErr != nil {
|
||||||
return *resErr
|
return *resErr
|
||||||
}
|
}
|
||||||
if r.DisplayName == "" {
|
|
||||||
return util.JSONResponse{
|
|
||||||
Code: http.StatusBadRequest,
|
|
||||||
JSON: jsonerror.BadJSON("'displayname' must be supplied."),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
localpart, domain, err := gomatrixserverlib.SplitID('@', userID)
|
localpart, domain, err := gomatrixserverlib.SplitID('@', userID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
util.GetLogger(req.Context()).WithError(err).Error("gomatrixserverlib.SplitID failed")
|
util.GetLogger(req.Context()).WithError(err).Error("gomatrixserverlib.SplitID failed")
|
||||||
return jsonerror.InternalServerError()
|
return util.JSONResponse{
|
||||||
|
Code: http.StatusInternalServerError,
|
||||||
|
JSON: spec.InternalServerError{},
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if !cfg.Matrix.IsLocalServerName(domain) {
|
if !cfg.Matrix.IsLocalServerName(domain) {
|
||||||
return util.JSONResponse{
|
return util.JSONResponse{
|
||||||
Code: http.StatusForbidden,
|
Code: http.StatusForbidden,
|
||||||
JSON: jsonerror.Forbidden("userID does not belong to a locally configured domain"),
|
JSON: spec.Forbidden("userID does not belong to a locally configured domain"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -229,29 +215,27 @@ func SetDisplayName(
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return util.JSONResponse{
|
return util.JSONResponse{
|
||||||
Code: http.StatusBadRequest,
|
Code: http.StatusBadRequest,
|
||||||
JSON: jsonerror.InvalidArgumentValue(err.Error()),
|
JSON: spec.InvalidParam(err.Error()),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
profileRes := &userapi.PerformUpdateDisplayNameResponse{}
|
profile, changed, err := profileAPI.SetDisplayName(req.Context(), localpart, domain, r.DisplayName)
|
||||||
err = profileAPI.SetDisplayName(req.Context(), &userapi.PerformUpdateDisplayNameRequest{
|
|
||||||
Localpart: localpart,
|
|
||||||
ServerName: domain,
|
|
||||||
DisplayName: r.DisplayName,
|
|
||||||
}, profileRes)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
util.GetLogger(req.Context()).WithError(err).Error("profileAPI.SetDisplayName failed")
|
util.GetLogger(req.Context()).WithError(err).Error("profileAPI.SetDisplayName failed")
|
||||||
return jsonerror.InternalServerError()
|
return util.JSONResponse{
|
||||||
|
Code: http.StatusInternalServerError,
|
||||||
|
JSON: spec.InternalServerError{},
|
||||||
|
}
|
||||||
}
|
}
|
||||||
// No need to build new membership events, since nothing changed
|
// No need to build new membership events, since nothing changed
|
||||||
if !profileRes.Changed {
|
if !changed {
|
||||||
return util.JSONResponse{
|
return util.JSONResponse{
|
||||||
Code: http.StatusOK,
|
Code: http.StatusOK,
|
||||||
JSON: struct{}{},
|
JSON: struct{}{},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
response, err := updateProfile(req.Context(), rsAPI, device, profileRes.Profile, userID, cfg, evTime)
|
response, err := updateProfile(req.Context(), rsAPI, device, profile, userID, evTime)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return response
|
return response
|
||||||
}
|
}
|
||||||
|
@ -265,42 +249,63 @@ func SetDisplayName(
|
||||||
func updateProfile(
|
func updateProfile(
|
||||||
ctx context.Context, rsAPI api.ClientRoomserverAPI, device *userapi.Device,
|
ctx context.Context, rsAPI api.ClientRoomserverAPI, device *userapi.Device,
|
||||||
profile *authtypes.Profile,
|
profile *authtypes.Profile,
|
||||||
userID string, cfg *config.ClientAPI, evTime time.Time,
|
userID string, evTime time.Time,
|
||||||
) (util.JSONResponse, error) {
|
) (util.JSONResponse, error) {
|
||||||
var res api.QueryRoomsForUserResponse
|
deviceUserID, err := spec.NewUserID(device.UserID, true)
|
||||||
err := rsAPI.QueryRoomsForUser(ctx, &api.QueryRoomsForUserRequest{
|
if err != nil {
|
||||||
UserID: device.UserID,
|
return util.JSONResponse{
|
||||||
WantMembership: "join",
|
Code: http.StatusInternalServerError,
|
||||||
}, &res)
|
JSON: spec.Unknown("internal server error"),
|
||||||
|
}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
rooms, err := rsAPI.QueryRoomsForUser(ctx, *deviceUserID, "join")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
util.GetLogger(ctx).WithError(err).Error("QueryRoomsForUser failed")
|
util.GetLogger(ctx).WithError(err).Error("QueryRoomsForUser failed")
|
||||||
return jsonerror.InternalServerError(), err
|
return util.JSONResponse{
|
||||||
|
Code: http.StatusInternalServerError,
|
||||||
|
JSON: spec.InternalServerError{},
|
||||||
|
}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
roomIDStrs := make([]string, len(rooms))
|
||||||
|
for i, room := range rooms {
|
||||||
|
roomIDStrs[i] = room.String()
|
||||||
}
|
}
|
||||||
|
|
||||||
_, domain, err := gomatrixserverlib.SplitID('@', userID)
|
_, domain, err := gomatrixserverlib.SplitID('@', userID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
util.GetLogger(ctx).WithError(err).Error("gomatrixserverlib.SplitID failed")
|
util.GetLogger(ctx).WithError(err).Error("gomatrixserverlib.SplitID failed")
|
||||||
return jsonerror.InternalServerError(), err
|
return util.JSONResponse{
|
||||||
|
Code: http.StatusInternalServerError,
|
||||||
|
JSON: spec.InternalServerError{},
|
||||||
|
}, err
|
||||||
}
|
}
|
||||||
|
|
||||||
events, err := buildMembershipEvents(
|
events, err := buildMembershipEvents(
|
||||||
ctx, device, res.RoomIDs, *profile, userID, cfg, evTime, rsAPI,
|
ctx, roomIDStrs, *profile, userID, evTime, rsAPI,
|
||||||
)
|
)
|
||||||
switch e := err.(type) {
|
switch e := err.(type) {
|
||||||
case nil:
|
case nil:
|
||||||
case gomatrixserverlib.BadJSONError:
|
case gomatrixserverlib.BadJSONError:
|
||||||
return util.JSONResponse{
|
return util.JSONResponse{
|
||||||
Code: http.StatusBadRequest,
|
Code: http.StatusBadRequest,
|
||||||
JSON: jsonerror.BadJSON(e.Error()),
|
JSON: spec.BadJSON(e.Error()),
|
||||||
}, e
|
}, e
|
||||||
default:
|
default:
|
||||||
util.GetLogger(ctx).WithError(err).Error("buildMembershipEvents failed")
|
util.GetLogger(ctx).WithError(err).Error("buildMembershipEvents failed")
|
||||||
return jsonerror.InternalServerError(), e
|
return util.JSONResponse{
|
||||||
|
Code: http.StatusInternalServerError,
|
||||||
|
JSON: spec.InternalServerError{},
|
||||||
|
}, e
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := api.SendEvents(ctx, rsAPI, api.KindNew, events, device.UserDomain(), domain, domain, nil, true); err != nil {
|
if err := api.SendEvents(ctx, rsAPI, api.KindNew, events, device.UserDomain(), domain, domain, nil, false); err != nil {
|
||||||
util.GetLogger(ctx).WithError(err).Error("SendEvents failed")
|
util.GetLogger(ctx).WithError(err).Error("SendEvents failed")
|
||||||
return jsonerror.InternalServerError(), err
|
return util.JSONResponse{
|
||||||
|
Code: http.StatusInternalServerError,
|
||||||
|
JSON: spec.InternalServerError{},
|
||||||
|
}, err
|
||||||
}
|
}
|
||||||
return util.JSONResponse{}, nil
|
return util.JSONResponse{}, nil
|
||||||
}
|
}
|
||||||
|
@ -308,12 +313,12 @@ func updateProfile(
|
||||||
// getProfile gets the full profile of a user by querying the database or a
|
// getProfile gets the full profile of a user by querying the database or a
|
||||||
// remote homeserver.
|
// remote homeserver.
|
||||||
// Returns an error when something goes wrong or specifically
|
// Returns an error when something goes wrong or specifically
|
||||||
// eventutil.ErrProfileNoExists when the profile doesn't exist.
|
// eventutil.ErrProfileNotExists when the profile doesn't exist.
|
||||||
func getProfile(
|
func getProfile(
|
||||||
ctx context.Context, profileAPI userapi.ClientUserAPI, cfg *config.ClientAPI,
|
ctx context.Context, profileAPI userapi.ProfileAPI, cfg *config.ClientAPI,
|
||||||
userID string,
|
userID string,
|
||||||
asAPI appserviceAPI.AppServiceInternalAPI,
|
asAPI appserviceAPI.AppServiceInternalAPI,
|
||||||
federation *gomatrixserverlib.FederationClient,
|
federation fclient.FederationClient,
|
||||||
) (*authtypes.Profile, error) {
|
) (*authtypes.Profile, error) {
|
||||||
localpart, domain, err := gomatrixserverlib.SplitID('@', userID)
|
localpart, domain, err := gomatrixserverlib.SplitID('@', userID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -325,7 +330,7 @@ func getProfile(
|
||||||
if fedErr != nil {
|
if fedErr != nil {
|
||||||
if x, ok := fedErr.(gomatrix.HTTPError); ok {
|
if x, ok := fedErr.(gomatrix.HTTPError); ok {
|
||||||
if x.Code == http.StatusNotFound {
|
if x.Code == http.StatusNotFound {
|
||||||
return nil, eventutil.ErrProfileNoExists
|
return nil, appserviceAPI.ErrProfileNotExists
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -349,49 +354,62 @@ func getProfile(
|
||||||
|
|
||||||
func buildMembershipEvents(
|
func buildMembershipEvents(
|
||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
device *userapi.Device,
|
|
||||||
roomIDs []string,
|
roomIDs []string,
|
||||||
newProfile authtypes.Profile, userID string, cfg *config.ClientAPI,
|
newProfile authtypes.Profile, userID string,
|
||||||
evTime time.Time, rsAPI api.ClientRoomserverAPI,
|
evTime time.Time, rsAPI api.ClientRoomserverAPI,
|
||||||
) ([]*gomatrixserverlib.HeaderedEvent, error) {
|
) ([]*types.HeaderedEvent, error) {
|
||||||
evs := []*gomatrixserverlib.HeaderedEvent{}
|
evs := []*types.HeaderedEvent{}
|
||||||
|
|
||||||
|
fullUserID, err := spec.NewUserID(userID, true)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
for _, roomID := range roomIDs {
|
for _, roomID := range roomIDs {
|
||||||
verReq := api.QueryRoomVersionForRoomRequest{RoomID: roomID}
|
validRoomID, err := spec.NewRoomID(roomID)
|
||||||
verRes := api.QueryRoomVersionForRoomResponse{}
|
if err != nil {
|
||||||
if err := rsAPI.QueryRoomVersionForRoom(ctx, &verReq, &verRes); err != nil {
|
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
senderID, err := rsAPI.QuerySenderIDForUser(ctx, *validRoomID, *fullUserID)
|
||||||
builder := gomatrixserverlib.EventBuilder{
|
if err != nil {
|
||||||
Sender: userID,
|
return nil, err
|
||||||
|
} else if senderID == nil {
|
||||||
|
return nil, fmt.Errorf("sender ID not found for %s in %s", *fullUserID, *validRoomID)
|
||||||
|
}
|
||||||
|
senderIDString := string(*senderID)
|
||||||
|
proto := gomatrixserverlib.ProtoEvent{
|
||||||
|
SenderID: senderIDString,
|
||||||
RoomID: roomID,
|
RoomID: roomID,
|
||||||
Type: "m.room.member",
|
Type: "m.room.member",
|
||||||
StateKey: &userID,
|
StateKey: &senderIDString,
|
||||||
}
|
}
|
||||||
|
|
||||||
content := gomatrixserverlib.MemberContent{
|
content := gomatrixserverlib.MemberContent{
|
||||||
Membership: gomatrixserverlib.Join,
|
Membership: spec.Join,
|
||||||
}
|
}
|
||||||
|
|
||||||
content.DisplayName = newProfile.DisplayName
|
content.DisplayName = newProfile.DisplayName
|
||||||
content.AvatarURL = newProfile.AvatarURL
|
content.AvatarURL = newProfile.AvatarURL
|
||||||
|
|
||||||
if err := builder.SetContent(content); err != nil {
|
if err = proto.SetContent(content); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
identity, err := cfg.Matrix.SigningIdentityFor(device.UserDomain())
|
user, err := spec.NewUserID(userID, true)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
event, err := eventutil.QueryAndBuildEvent(ctx, &builder, cfg.Matrix, identity, evTime, rsAPI, nil)
|
identity, err := rsAPI.SigningIdentityFor(ctx, *validRoomID, *user)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
evs = append(evs, event.Headered(verRes.RoomVersion))
|
event, err := eventutil.QueryAndBuildEvent(ctx, &proto, &identity, evTime, rsAPI, nil)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
evs = append(evs, event)
|
||||||
}
|
}
|
||||||
|
|
||||||
return evs, nil
|
return evs, nil
|
||||||
|
|
|
@ -19,9 +19,9 @@ import (
|
||||||
"net/url"
|
"net/url"
|
||||||
|
|
||||||
"github.com/matrix-org/dendrite/clientapi/httputil"
|
"github.com/matrix-org/dendrite/clientapi/httputil"
|
||||||
"github.com/matrix-org/dendrite/clientapi/jsonerror"
|
|
||||||
userapi "github.com/matrix-org/dendrite/userapi/api"
|
userapi "github.com/matrix-org/dendrite/userapi/api"
|
||||||
"github.com/matrix-org/gomatrixserverlib"
|
"github.com/matrix-org/gomatrixserverlib"
|
||||||
|
"github.com/matrix-org/gomatrixserverlib/spec"
|
||||||
"github.com/matrix-org/util"
|
"github.com/matrix-org/util"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -34,7 +34,10 @@ func GetPushers(
|
||||||
localpart, domain, err := gomatrixserverlib.SplitID('@', device.UserID)
|
localpart, domain, err := gomatrixserverlib.SplitID('@', device.UserID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
util.GetLogger(req.Context()).WithError(err).Error("SplitID failed")
|
util.GetLogger(req.Context()).WithError(err).Error("SplitID failed")
|
||||||
return jsonerror.InternalServerError()
|
return util.JSONResponse{
|
||||||
|
Code: http.StatusInternalServerError,
|
||||||
|
JSON: spec.InternalServerError{},
|
||||||
|
}
|
||||||
}
|
}
|
||||||
err = userAPI.QueryPushers(req.Context(), &userapi.QueryPushersRequest{
|
err = userAPI.QueryPushers(req.Context(), &userapi.QueryPushersRequest{
|
||||||
Localpart: localpart,
|
Localpart: localpart,
|
||||||
|
@ -42,7 +45,10 @@ func GetPushers(
|
||||||
}, &queryRes)
|
}, &queryRes)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
util.GetLogger(req.Context()).WithError(err).Error("QueryPushers failed")
|
util.GetLogger(req.Context()).WithError(err).Error("QueryPushers failed")
|
||||||
return jsonerror.InternalServerError()
|
return util.JSONResponse{
|
||||||
|
Code: http.StatusInternalServerError,
|
||||||
|
JSON: spec.InternalServerError{},
|
||||||
|
}
|
||||||
}
|
}
|
||||||
for i := range queryRes.Pushers {
|
for i := range queryRes.Pushers {
|
||||||
queryRes.Pushers[i].SessionID = 0
|
queryRes.Pushers[i].SessionID = 0
|
||||||
|
@ -63,7 +69,10 @@ func SetPusher(
|
||||||
localpart, domain, err := gomatrixserverlib.SplitID('@', device.UserID)
|
localpart, domain, err := gomatrixserverlib.SplitID('@', device.UserID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
util.GetLogger(req.Context()).WithError(err).Error("SplitID failed")
|
util.GetLogger(req.Context()).WithError(err).Error("SplitID failed")
|
||||||
return jsonerror.InternalServerError()
|
return util.JSONResponse{
|
||||||
|
Code: http.StatusInternalServerError,
|
||||||
|
JSON: spec.InternalServerError{},
|
||||||
|
}
|
||||||
}
|
}
|
||||||
body := userapi.PerformPusherSetRequest{}
|
body := userapi.PerformPusherSetRequest{}
|
||||||
if resErr := httputil.UnmarshalJSONRequest(req, &body); resErr != nil {
|
if resErr := httputil.UnmarshalJSONRequest(req, &body); resErr != nil {
|
||||||
|
@ -99,7 +108,10 @@ func SetPusher(
|
||||||
err = userAPI.PerformPusherSet(req.Context(), &body, &struct{}{})
|
err = userAPI.PerformPusherSet(req.Context(), &body, &struct{}{})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
util.GetLogger(req.Context()).WithError(err).Error("PerformPusherSet failed")
|
util.GetLogger(req.Context()).WithError(err).Error("PerformPusherSet failed")
|
||||||
return jsonerror.InternalServerError()
|
return util.JSONResponse{
|
||||||
|
Code: http.StatusInternalServerError,
|
||||||
|
JSON: spec.InternalServerError{},
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return util.JSONResponse{
|
return util.JSONResponse{
|
||||||
|
@ -111,6 +123,6 @@ func SetPusher(
|
||||||
func invalidParam(msg string) util.JSONResponse {
|
func invalidParam(msg string) util.JSONResponse {
|
||||||
return util.JSONResponse{
|
return util.JSONResponse{
|
||||||
Code: http.StatusBadRequest,
|
Code: http.StatusBadRequest,
|
||||||
JSON: jsonerror.InvalidParam(msg),
|
JSON: spec.InvalidParam(msg),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,31 +7,34 @@ import (
|
||||||
"net/http"
|
"net/http"
|
||||||
"reflect"
|
"reflect"
|
||||||
|
|
||||||
"github.com/matrix-org/dendrite/clientapi/jsonerror"
|
|
||||||
"github.com/matrix-org/dendrite/internal/pushrules"
|
"github.com/matrix-org/dendrite/internal/pushrules"
|
||||||
userapi "github.com/matrix-org/dendrite/userapi/api"
|
userapi "github.com/matrix-org/dendrite/userapi/api"
|
||||||
|
"github.com/matrix-org/gomatrixserverlib/spec"
|
||||||
"github.com/matrix-org/util"
|
"github.com/matrix-org/util"
|
||||||
)
|
)
|
||||||
|
|
||||||
func errorResponse(ctx context.Context, err error, msg string, args ...interface{}) util.JSONResponse {
|
func errorResponse(ctx context.Context, err error, msg string, args ...interface{}) util.JSONResponse {
|
||||||
if eerr, ok := err.(*jsonerror.MatrixError); ok {
|
if eerr, ok := err.(spec.MatrixError); ok {
|
||||||
var status int
|
var status int
|
||||||
switch eerr.ErrCode {
|
switch eerr.ErrCode {
|
||||||
case "M_INVALID_ARGUMENT_VALUE":
|
case spec.ErrorInvalidParam:
|
||||||
status = http.StatusBadRequest
|
status = http.StatusBadRequest
|
||||||
case "M_NOT_FOUND":
|
case spec.ErrorNotFound:
|
||||||
status = http.StatusNotFound
|
status = http.StatusNotFound
|
||||||
default:
|
default:
|
||||||
status = http.StatusInternalServerError
|
status = http.StatusInternalServerError
|
||||||
}
|
}
|
||||||
return util.MatrixErrorResponse(status, eerr.ErrCode, eerr.Err)
|
return util.MatrixErrorResponse(status, string(eerr.ErrCode), eerr.Err)
|
||||||
}
|
}
|
||||||
util.GetLogger(ctx).WithError(err).Errorf(msg, args...)
|
util.GetLogger(ctx).WithError(err).Errorf(msg, args...)
|
||||||
return jsonerror.InternalServerError()
|
return util.JSONResponse{
|
||||||
|
Code: http.StatusInternalServerError,
|
||||||
|
JSON: spec.InternalServerError{},
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func GetAllPushRules(ctx context.Context, device *userapi.Device, userAPI userapi.ClientUserAPI) util.JSONResponse {
|
func GetAllPushRules(ctx context.Context, device *userapi.Device, userAPI userapi.ClientUserAPI) util.JSONResponse {
|
||||||
ruleSets, err := queryPushRules(ctx, device.UserID, userAPI)
|
ruleSets, err := userAPI.QueryPushRules(ctx, device.UserID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errorResponse(ctx, err, "queryPushRulesJSON failed")
|
return errorResponse(ctx, err, "queryPushRulesJSON failed")
|
||||||
}
|
}
|
||||||
|
@ -42,13 +45,13 @@ func GetAllPushRules(ctx context.Context, device *userapi.Device, userAPI userap
|
||||||
}
|
}
|
||||||
|
|
||||||
func GetPushRulesByScope(ctx context.Context, scope string, device *userapi.Device, userAPI userapi.ClientUserAPI) util.JSONResponse {
|
func GetPushRulesByScope(ctx context.Context, scope string, device *userapi.Device, userAPI userapi.ClientUserAPI) util.JSONResponse {
|
||||||
ruleSets, err := queryPushRules(ctx, device.UserID, userAPI)
|
ruleSets, err := userAPI.QueryPushRules(ctx, device.UserID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errorResponse(ctx, err, "queryPushRulesJSON failed")
|
return errorResponse(ctx, err, "queryPushRulesJSON failed")
|
||||||
}
|
}
|
||||||
ruleSet := pushRuleSetByScope(ruleSets, pushrules.Scope(scope))
|
ruleSet := pushRuleSetByScope(ruleSets, pushrules.Scope(scope))
|
||||||
if ruleSet == nil {
|
if ruleSet == nil {
|
||||||
return errorResponse(ctx, jsonerror.InvalidArgumentValue("invalid push rule set"), "pushRuleSetByScope failed")
|
return errorResponse(ctx, spec.InvalidParam("invalid push rule set"), "pushRuleSetByScope failed")
|
||||||
}
|
}
|
||||||
return util.JSONResponse{
|
return util.JSONResponse{
|
||||||
Code: http.StatusOK,
|
Code: http.StatusOK,
|
||||||
|
@ -57,17 +60,18 @@ func GetPushRulesByScope(ctx context.Context, scope string, device *userapi.Devi
|
||||||
}
|
}
|
||||||
|
|
||||||
func GetPushRulesByKind(ctx context.Context, scope, kind string, device *userapi.Device, userAPI userapi.ClientUserAPI) util.JSONResponse {
|
func GetPushRulesByKind(ctx context.Context, scope, kind string, device *userapi.Device, userAPI userapi.ClientUserAPI) util.JSONResponse {
|
||||||
ruleSets, err := queryPushRules(ctx, device.UserID, userAPI)
|
ruleSets, err := userAPI.QueryPushRules(ctx, device.UserID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errorResponse(ctx, err, "queryPushRules failed")
|
return errorResponse(ctx, err, "queryPushRules failed")
|
||||||
}
|
}
|
||||||
ruleSet := pushRuleSetByScope(ruleSets, pushrules.Scope(scope))
|
ruleSet := pushRuleSetByScope(ruleSets, pushrules.Scope(scope))
|
||||||
if ruleSet == nil {
|
if ruleSet == nil {
|
||||||
return errorResponse(ctx, jsonerror.InvalidArgumentValue("invalid push rule set"), "pushRuleSetByScope failed")
|
return errorResponse(ctx, spec.InvalidParam("invalid push rule set"), "pushRuleSetByScope failed")
|
||||||
}
|
}
|
||||||
rulesPtr := pushRuleSetKindPointer(ruleSet, pushrules.Kind(kind))
|
rulesPtr := pushRuleSetKindPointer(ruleSet, pushrules.Kind(kind))
|
||||||
if rulesPtr == nil {
|
// Even if rulesPtr is not nil, there may not be any rules for this kind
|
||||||
return errorResponse(ctx, jsonerror.InvalidArgumentValue("invalid push rules kind"), "pushRuleSetKindPointer failed")
|
if rulesPtr == nil || (rulesPtr != nil && len(*rulesPtr) == 0) {
|
||||||
|
return errorResponse(ctx, spec.InvalidParam("invalid push rules kind"), "pushRuleSetKindPointer failed")
|
||||||
}
|
}
|
||||||
return util.JSONResponse{
|
return util.JSONResponse{
|
||||||
Code: http.StatusOK,
|
Code: http.StatusOK,
|
||||||
|
@ -76,21 +80,21 @@ func GetPushRulesByKind(ctx context.Context, scope, kind string, device *userapi
|
||||||
}
|
}
|
||||||
|
|
||||||
func GetPushRuleByRuleID(ctx context.Context, scope, kind, ruleID string, device *userapi.Device, userAPI userapi.ClientUserAPI) util.JSONResponse {
|
func GetPushRuleByRuleID(ctx context.Context, scope, kind, ruleID string, device *userapi.Device, userAPI userapi.ClientUserAPI) util.JSONResponse {
|
||||||
ruleSets, err := queryPushRules(ctx, device.UserID, userAPI)
|
ruleSets, err := userAPI.QueryPushRules(ctx, device.UserID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errorResponse(ctx, err, "queryPushRules failed")
|
return errorResponse(ctx, err, "queryPushRules failed")
|
||||||
}
|
}
|
||||||
ruleSet := pushRuleSetByScope(ruleSets, pushrules.Scope(scope))
|
ruleSet := pushRuleSetByScope(ruleSets, pushrules.Scope(scope))
|
||||||
if ruleSet == nil {
|
if ruleSet == nil {
|
||||||
return errorResponse(ctx, jsonerror.InvalidArgumentValue("invalid push rule set"), "pushRuleSetByScope failed")
|
return errorResponse(ctx, spec.InvalidParam("invalid push rule set"), "pushRuleSetByScope failed")
|
||||||
}
|
}
|
||||||
rulesPtr := pushRuleSetKindPointer(ruleSet, pushrules.Kind(kind))
|
rulesPtr := pushRuleSetKindPointer(ruleSet, pushrules.Kind(kind))
|
||||||
if rulesPtr == nil {
|
if rulesPtr == nil {
|
||||||
return errorResponse(ctx, jsonerror.InvalidArgumentValue("invalid push rules kind"), "pushRuleSetKindPointer failed")
|
return errorResponse(ctx, spec.InvalidParam("invalid push rules kind"), "pushRuleSetKindPointer failed")
|
||||||
}
|
}
|
||||||
i := pushRuleIndexByID(*rulesPtr, ruleID)
|
i := pushRuleIndexByID(*rulesPtr, ruleID)
|
||||||
if i < 0 {
|
if i < 0 {
|
||||||
return errorResponse(ctx, jsonerror.NotFound("push rule ID not found"), "pushRuleIndexByID failed")
|
return errorResponse(ctx, spec.NotFound("push rule ID not found"), "pushRuleIndexByID failed")
|
||||||
}
|
}
|
||||||
return util.JSONResponse{
|
return util.JSONResponse{
|
||||||
Code: http.StatusOK,
|
Code: http.StatusOK,
|
||||||
|
@ -101,26 +105,30 @@ func GetPushRuleByRuleID(ctx context.Context, scope, kind, ruleID string, device
|
||||||
func PutPushRuleByRuleID(ctx context.Context, scope, kind, ruleID, afterRuleID, beforeRuleID string, body io.Reader, device *userapi.Device, userAPI userapi.ClientUserAPI) util.JSONResponse {
|
func PutPushRuleByRuleID(ctx context.Context, scope, kind, ruleID, afterRuleID, beforeRuleID string, body io.Reader, device *userapi.Device, userAPI userapi.ClientUserAPI) util.JSONResponse {
|
||||||
var newRule pushrules.Rule
|
var newRule pushrules.Rule
|
||||||
if err := json.NewDecoder(body).Decode(&newRule); err != nil {
|
if err := json.NewDecoder(body).Decode(&newRule); err != nil {
|
||||||
return errorResponse(ctx, err, "JSON Decode failed")
|
return util.JSONResponse{
|
||||||
|
Code: http.StatusBadRequest,
|
||||||
|
JSON: spec.BadJSON(err.Error()),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
newRule.RuleID = ruleID
|
newRule.RuleID = ruleID
|
||||||
|
|
||||||
errs := pushrules.ValidateRule(pushrules.Kind(kind), &newRule)
|
errs := pushrules.ValidateRule(pushrules.Kind(kind), &newRule)
|
||||||
if len(errs) > 0 {
|
if len(errs) > 0 {
|
||||||
return errorResponse(ctx, jsonerror.InvalidArgumentValue(errs[0].Error()), "rule sanity check failed: %v", errs)
|
return errorResponse(ctx, spec.InvalidParam(errs[0].Error()), "rule sanity check failed: %v", errs)
|
||||||
}
|
}
|
||||||
|
|
||||||
ruleSets, err := queryPushRules(ctx, device.UserID, userAPI)
|
ruleSets, err := userAPI.QueryPushRules(ctx, device.UserID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errorResponse(ctx, err, "queryPushRules failed")
|
return errorResponse(ctx, err, "queryPushRules failed")
|
||||||
}
|
}
|
||||||
ruleSet := pushRuleSetByScope(ruleSets, pushrules.Scope(scope))
|
ruleSet := pushRuleSetByScope(ruleSets, pushrules.Scope(scope))
|
||||||
if ruleSet == nil {
|
if ruleSet == nil {
|
||||||
return errorResponse(ctx, jsonerror.InvalidArgumentValue("invalid push rule set"), "pushRuleSetByScope failed")
|
return errorResponse(ctx, spec.InvalidParam("invalid push rule set"), "pushRuleSetByScope failed")
|
||||||
}
|
}
|
||||||
rulesPtr := pushRuleSetKindPointer(ruleSet, pushrules.Kind(kind))
|
rulesPtr := pushRuleSetKindPointer(ruleSet, pushrules.Kind(kind))
|
||||||
if rulesPtr == nil {
|
if rulesPtr == nil {
|
||||||
return errorResponse(ctx, jsonerror.InvalidArgumentValue("invalid push rules kind"), "pushRuleSetKindPointer failed")
|
// while this should be impossible (ValidateRule would already return an error), better keep it around
|
||||||
|
return errorResponse(ctx, spec.InvalidParam("invalid push rules kind"), "pushRuleSetKindPointer failed")
|
||||||
}
|
}
|
||||||
i := pushRuleIndexByID(*rulesPtr, ruleID)
|
i := pushRuleIndexByID(*rulesPtr, ruleID)
|
||||||
if i >= 0 && afterRuleID == "" && beforeRuleID == "" {
|
if i >= 0 && afterRuleID == "" && beforeRuleID == "" {
|
||||||
|
@ -144,7 +152,7 @@ func PutPushRuleByRuleID(ctx context.Context, scope, kind, ruleID, afterRuleID,
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add new rule.
|
// Add new rule.
|
||||||
i, err := findPushRuleInsertionIndex(*rulesPtr, afterRuleID, beforeRuleID)
|
i, err = findPushRuleInsertionIndex(*rulesPtr, afterRuleID, beforeRuleID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errorResponse(ctx, err, "findPushRuleInsertionIndex failed")
|
return errorResponse(ctx, err, "findPushRuleInsertionIndex failed")
|
||||||
}
|
}
|
||||||
|
@ -153,7 +161,7 @@ func PutPushRuleByRuleID(ctx context.Context, scope, kind, ruleID, afterRuleID,
|
||||||
util.GetLogger(ctx).WithField("after", afterRuleID).WithField("before", beforeRuleID).Infof("Added new push rule at %d", i)
|
util.GetLogger(ctx).WithField("after", afterRuleID).WithField("before", beforeRuleID).Infof("Added new push rule at %d", i)
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := putPushRules(ctx, device.UserID, ruleSets, userAPI); err != nil {
|
if err = userAPI.PerformPushRulesPut(ctx, device.UserID, ruleSets); err != nil {
|
||||||
return errorResponse(ctx, err, "putPushRules failed")
|
return errorResponse(ctx, err, "putPushRules failed")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -161,26 +169,26 @@ func PutPushRuleByRuleID(ctx context.Context, scope, kind, ruleID, afterRuleID,
|
||||||
}
|
}
|
||||||
|
|
||||||
func DeletePushRuleByRuleID(ctx context.Context, scope, kind, ruleID string, device *userapi.Device, userAPI userapi.ClientUserAPI) util.JSONResponse {
|
func DeletePushRuleByRuleID(ctx context.Context, scope, kind, ruleID string, device *userapi.Device, userAPI userapi.ClientUserAPI) util.JSONResponse {
|
||||||
ruleSets, err := queryPushRules(ctx, device.UserID, userAPI)
|
ruleSets, err := userAPI.QueryPushRules(ctx, device.UserID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errorResponse(ctx, err, "queryPushRules failed")
|
return errorResponse(ctx, err, "queryPushRules failed")
|
||||||
}
|
}
|
||||||
ruleSet := pushRuleSetByScope(ruleSets, pushrules.Scope(scope))
|
ruleSet := pushRuleSetByScope(ruleSets, pushrules.Scope(scope))
|
||||||
if ruleSet == nil {
|
if ruleSet == nil {
|
||||||
return errorResponse(ctx, jsonerror.InvalidArgumentValue("invalid push rule set"), "pushRuleSetByScope failed")
|
return errorResponse(ctx, spec.InvalidParam("invalid push rule set"), "pushRuleSetByScope failed")
|
||||||
}
|
}
|
||||||
rulesPtr := pushRuleSetKindPointer(ruleSet, pushrules.Kind(kind))
|
rulesPtr := pushRuleSetKindPointer(ruleSet, pushrules.Kind(kind))
|
||||||
if rulesPtr == nil {
|
if rulesPtr == nil {
|
||||||
return errorResponse(ctx, jsonerror.InvalidArgumentValue("invalid push rules kind"), "pushRuleSetKindPointer failed")
|
return errorResponse(ctx, spec.InvalidParam("invalid push rules kind"), "pushRuleSetKindPointer failed")
|
||||||
}
|
}
|
||||||
i := pushRuleIndexByID(*rulesPtr, ruleID)
|
i := pushRuleIndexByID(*rulesPtr, ruleID)
|
||||||
if i < 0 {
|
if i < 0 {
|
||||||
return errorResponse(ctx, jsonerror.NotFound("push rule ID not found"), "pushRuleIndexByID failed")
|
return errorResponse(ctx, spec.NotFound("push rule ID not found"), "pushRuleIndexByID failed")
|
||||||
}
|
}
|
||||||
|
|
||||||
*rulesPtr = append((*rulesPtr)[:i], (*rulesPtr)[i+1:]...)
|
*rulesPtr = append((*rulesPtr)[:i], (*rulesPtr)[i+1:]...)
|
||||||
|
|
||||||
if err := putPushRules(ctx, device.UserID, ruleSets, userAPI); err != nil {
|
if err = userAPI.PerformPushRulesPut(ctx, device.UserID, ruleSets); err != nil {
|
||||||
return errorResponse(ctx, err, "putPushRules failed")
|
return errorResponse(ctx, err, "putPushRules failed")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -192,21 +200,21 @@ func GetPushRuleAttrByRuleID(ctx context.Context, scope, kind, ruleID, attr stri
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errorResponse(ctx, err, "pushRuleAttrGetter failed")
|
return errorResponse(ctx, err, "pushRuleAttrGetter failed")
|
||||||
}
|
}
|
||||||
ruleSets, err := queryPushRules(ctx, device.UserID, userAPI)
|
ruleSets, err := userAPI.QueryPushRules(ctx, device.UserID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errorResponse(ctx, err, "queryPushRules failed")
|
return errorResponse(ctx, err, "queryPushRules failed")
|
||||||
}
|
}
|
||||||
ruleSet := pushRuleSetByScope(ruleSets, pushrules.Scope(scope))
|
ruleSet := pushRuleSetByScope(ruleSets, pushrules.Scope(scope))
|
||||||
if ruleSet == nil {
|
if ruleSet == nil {
|
||||||
return errorResponse(ctx, jsonerror.InvalidArgumentValue("invalid push rule set"), "pushRuleSetByScope failed")
|
return errorResponse(ctx, spec.InvalidParam("invalid push rule set"), "pushRuleSetByScope failed")
|
||||||
}
|
}
|
||||||
rulesPtr := pushRuleSetKindPointer(ruleSet, pushrules.Kind(kind))
|
rulesPtr := pushRuleSetKindPointer(ruleSet, pushrules.Kind(kind))
|
||||||
if rulesPtr == nil {
|
if rulesPtr == nil {
|
||||||
return errorResponse(ctx, jsonerror.InvalidArgumentValue("invalid push rules kind"), "pushRuleSetKindPointer failed")
|
return errorResponse(ctx, spec.InvalidParam("invalid push rules kind"), "pushRuleSetKindPointer failed")
|
||||||
}
|
}
|
||||||
i := pushRuleIndexByID(*rulesPtr, ruleID)
|
i := pushRuleIndexByID(*rulesPtr, ruleID)
|
||||||
if i < 0 {
|
if i < 0 {
|
||||||
return errorResponse(ctx, jsonerror.NotFound("push rule ID not found"), "pushRuleIndexByID failed")
|
return errorResponse(ctx, spec.NotFound("push rule ID not found"), "pushRuleIndexByID failed")
|
||||||
}
|
}
|
||||||
return util.JSONResponse{
|
return util.JSONResponse{
|
||||||
Code: http.StatusOK,
|
Code: http.StatusOK,
|
||||||
|
@ -221,7 +229,7 @@ func PutPushRuleAttrByRuleID(ctx context.Context, scope, kind, ruleID, attr stri
|
||||||
if err := json.NewDecoder(body).Decode(&newPartialRule); err != nil {
|
if err := json.NewDecoder(body).Decode(&newPartialRule); err != nil {
|
||||||
return util.JSONResponse{
|
return util.JSONResponse{
|
||||||
Code: http.StatusBadRequest,
|
Code: http.StatusBadRequest,
|
||||||
JSON: jsonerror.BadJSON(err.Error()),
|
JSON: spec.BadJSON(err.Error()),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if newPartialRule.Actions == nil {
|
if newPartialRule.Actions == nil {
|
||||||
|
@ -238,27 +246,27 @@ func PutPushRuleAttrByRuleID(ctx context.Context, scope, kind, ruleID, attr stri
|
||||||
return errorResponse(ctx, err, "pushRuleAttrSetter failed")
|
return errorResponse(ctx, err, "pushRuleAttrSetter failed")
|
||||||
}
|
}
|
||||||
|
|
||||||
ruleSets, err := queryPushRules(ctx, device.UserID, userAPI)
|
ruleSets, err := userAPI.QueryPushRules(ctx, device.UserID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errorResponse(ctx, err, "queryPushRules failed")
|
return errorResponse(ctx, err, "queryPushRules failed")
|
||||||
}
|
}
|
||||||
ruleSet := pushRuleSetByScope(ruleSets, pushrules.Scope(scope))
|
ruleSet := pushRuleSetByScope(ruleSets, pushrules.Scope(scope))
|
||||||
if ruleSet == nil {
|
if ruleSet == nil {
|
||||||
return errorResponse(ctx, jsonerror.InvalidArgumentValue("invalid push rule set"), "pushRuleSetByScope failed")
|
return errorResponse(ctx, spec.InvalidParam("invalid push rule set"), "pushRuleSetByScope failed")
|
||||||
}
|
}
|
||||||
rulesPtr := pushRuleSetKindPointer(ruleSet, pushrules.Kind(kind))
|
rulesPtr := pushRuleSetKindPointer(ruleSet, pushrules.Kind(kind))
|
||||||
if rulesPtr == nil {
|
if rulesPtr == nil {
|
||||||
return errorResponse(ctx, jsonerror.InvalidArgumentValue("invalid push rules kind"), "pushRuleSetKindPointer failed")
|
return errorResponse(ctx, spec.InvalidParam("invalid push rules kind"), "pushRuleSetKindPointer failed")
|
||||||
}
|
}
|
||||||
i := pushRuleIndexByID(*rulesPtr, ruleID)
|
i := pushRuleIndexByID(*rulesPtr, ruleID)
|
||||||
if i < 0 {
|
if i < 0 {
|
||||||
return errorResponse(ctx, jsonerror.NotFound("push rule ID not found"), "pushRuleIndexByID failed")
|
return errorResponse(ctx, spec.NotFound("push rule ID not found"), "pushRuleIndexByID failed")
|
||||||
}
|
}
|
||||||
|
|
||||||
if !reflect.DeepEqual(attrGet((*rulesPtr)[i]), attrGet(&newPartialRule)) {
|
if !reflect.DeepEqual(attrGet((*rulesPtr)[i]), attrGet(&newPartialRule)) {
|
||||||
attrSet((*rulesPtr)[i], &newPartialRule)
|
attrSet((*rulesPtr)[i], &newPartialRule)
|
||||||
|
|
||||||
if err := putPushRules(ctx, device.UserID, ruleSets, userAPI); err != nil {
|
if err = userAPI.PerformPushRulesPut(ctx, device.UserID, ruleSets); err != nil {
|
||||||
return errorResponse(ctx, err, "putPushRules failed")
|
return errorResponse(ctx, err, "putPushRules failed")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -266,28 +274,6 @@ func PutPushRuleAttrByRuleID(ctx context.Context, scope, kind, ruleID, attr stri
|
||||||
return util.JSONResponse{Code: http.StatusOK, JSON: struct{}{}}
|
return util.JSONResponse{Code: http.StatusOK, JSON: struct{}{}}
|
||||||
}
|
}
|
||||||
|
|
||||||
func queryPushRules(ctx context.Context, userID string, userAPI userapi.ClientUserAPI) (*pushrules.AccountRuleSets, error) {
|
|
||||||
var res userapi.QueryPushRulesResponse
|
|
||||||
if err := userAPI.QueryPushRules(ctx, &userapi.QueryPushRulesRequest{UserID: userID}, &res); err != nil {
|
|
||||||
util.GetLogger(ctx).WithError(err).Error("userAPI.QueryPushRules failed")
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return res.RuleSets, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func putPushRules(ctx context.Context, userID string, ruleSets *pushrules.AccountRuleSets, userAPI userapi.ClientUserAPI) error {
|
|
||||||
req := userapi.PerformPushRulesPutRequest{
|
|
||||||
UserID: userID,
|
|
||||||
RuleSets: ruleSets,
|
|
||||||
}
|
|
||||||
var res struct{}
|
|
||||||
if err := userAPI.PerformPushRulesPut(ctx, &req, &res); err != nil {
|
|
||||||
util.GetLogger(ctx).WithError(err).Error("userAPI.PerformPushRulesPut failed")
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func pushRuleSetByScope(ruleSets *pushrules.AccountRuleSets, scope pushrules.Scope) *pushrules.RuleSet {
|
func pushRuleSetByScope(ruleSets *pushrules.AccountRuleSets, scope pushrules.Scope) *pushrules.RuleSet {
|
||||||
switch scope {
|
switch scope {
|
||||||
case pushrules.GlobalScope:
|
case pushrules.GlobalScope:
|
||||||
|
@ -330,7 +316,7 @@ func pushRuleAttrGetter(attr string) (func(*pushrules.Rule) interface{}, error)
|
||||||
case "enabled":
|
case "enabled":
|
||||||
return func(rule *pushrules.Rule) interface{} { return rule.Enabled }, nil
|
return func(rule *pushrules.Rule) interface{} { return rule.Enabled }, nil
|
||||||
default:
|
default:
|
||||||
return nil, jsonerror.InvalidArgumentValue("invalid push rule attribute")
|
return nil, spec.InvalidParam("invalid push rule attribute")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -341,7 +327,7 @@ func pushRuleAttrSetter(attr string) (func(dest, src *pushrules.Rule), error) {
|
||||||
case "enabled":
|
case "enabled":
|
||||||
return func(dest, src *pushrules.Rule) { dest.Enabled = src.Enabled }, nil
|
return func(dest, src *pushrules.Rule) { dest.Enabled = src.Enabled }, nil
|
||||||
default:
|
default:
|
||||||
return nil, jsonerror.InvalidArgumentValue("invalid push rule attribute")
|
return nil, spec.InvalidParam("invalid push rule attribute")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -355,10 +341,10 @@ func findPushRuleInsertionIndex(rules []*pushrules.Rule, afterID, beforeID strin
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if i == len(rules) {
|
if i == len(rules) {
|
||||||
return 0, jsonerror.NotFound("after: rule ID not found")
|
return 0, spec.NotFound("after: rule ID not found")
|
||||||
}
|
}
|
||||||
if rules[i].Default {
|
if rules[i].Default {
|
||||||
return 0, jsonerror.NotFound("after: rule ID must not be a default rule")
|
return 0, spec.NotFound("after: rule ID must not be a default rule")
|
||||||
}
|
}
|
||||||
// We stopped on the "after" match to differentiate
|
// We stopped on the "after" match to differentiate
|
||||||
// not-found from is-last-entry. Now we move to the earliest
|
// not-found from is-last-entry. Now we move to the earliest
|
||||||
|
@ -373,10 +359,10 @@ func findPushRuleInsertionIndex(rules []*pushrules.Rule, afterID, beforeID strin
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if i == len(rules) {
|
if i == len(rules) {
|
||||||
return 0, jsonerror.NotFound("before: rule ID not found")
|
return 0, spec.NotFound("before: rule ID not found")
|
||||||
}
|
}
|
||||||
if rules[i].Default {
|
if rules[i].Default {
|
||||||
return 0, jsonerror.NotFound("before: rule ID must not be a default rule")
|
return 0, spec.NotFound("before: rule ID must not be a default rule")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -20,18 +20,16 @@ import (
|
||||||
"net/http"
|
"net/http"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/matrix-org/dendrite/clientapi/jsonerror"
|
|
||||||
"github.com/matrix-org/dendrite/clientapi/producers"
|
"github.com/matrix-org/dendrite/clientapi/producers"
|
||||||
"github.com/matrix-org/gomatrixserverlib"
|
"github.com/matrix-org/gomatrixserverlib/spec"
|
||||||
|
|
||||||
"github.com/matrix-org/dendrite/userapi/api"
|
|
||||||
userapi "github.com/matrix-org/dendrite/userapi/api"
|
userapi "github.com/matrix-org/dendrite/userapi/api"
|
||||||
"github.com/matrix-org/util"
|
"github.com/matrix-org/util"
|
||||||
"github.com/sirupsen/logrus"
|
"github.com/sirupsen/logrus"
|
||||||
)
|
)
|
||||||
|
|
||||||
func SetReceipt(req *http.Request, userAPI api.ClientUserAPI, syncProducer *producers.SyncAPIProducer, device *userapi.Device, roomID, receiptType, eventID string) util.JSONResponse {
|
func SetReceipt(req *http.Request, userAPI userapi.ClientUserAPI, syncProducer *producers.SyncAPIProducer, device *userapi.Device, roomID, receiptType, eventID string) util.JSONResponse {
|
||||||
timestamp := gomatrixserverlib.AsTimestamp(time.Now())
|
timestamp := spec.AsTimestamp(time.Now())
|
||||||
logrus.WithFields(logrus.Fields{
|
logrus.WithFields(logrus.Fields{
|
||||||
"roomID": roomID,
|
"roomID": roomID,
|
||||||
"receiptType": receiptType,
|
"receiptType": receiptType,
|
||||||
|
@ -49,16 +47,19 @@ func SetReceipt(req *http.Request, userAPI api.ClientUserAPI, syncProducer *prod
|
||||||
case "m.fully_read":
|
case "m.fully_read":
|
||||||
data, err := json.Marshal(fullyReadEvent{EventID: eventID})
|
data, err := json.Marshal(fullyReadEvent{EventID: eventID})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return jsonerror.InternalServerError()
|
return util.JSONResponse{
|
||||||
|
Code: http.StatusInternalServerError,
|
||||||
|
JSON: spec.InternalServerError{},
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
dataReq := api.InputAccountDataRequest{
|
dataReq := userapi.InputAccountDataRequest{
|
||||||
UserID: device.UserID,
|
UserID: device.UserID,
|
||||||
DataType: "m.fully_read",
|
DataType: "m.fully_read",
|
||||||
RoomID: roomID,
|
RoomID: roomID,
|
||||||
AccountData: data,
|
AccountData: data,
|
||||||
}
|
}
|
||||||
dataRes := api.InputAccountDataResponse{}
|
dataRes := userapi.InputAccountDataResponse{}
|
||||||
if err := userAPI.InputAccountData(req.Context(), &dataReq, &dataRes); err != nil {
|
if err := userAPI.InputAccountData(req.Context(), &dataReq, &dataRes); err != nil {
|
||||||
util.GetLogger(req.Context()).WithError(err).Error("userAPI.InputAccountData failed")
|
util.GetLogger(req.Context()).WithError(err).Error("userAPI.InputAccountData failed")
|
||||||
return util.ErrorResponse(err)
|
return util.ErrorResponse(err)
|
||||||
|
|
|
@ -16,23 +16,26 @@ package routing
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"errors"
|
||||||
"net/http"
|
"net/http"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/matrix-org/gomatrixserverlib"
|
"github.com/matrix-org/gomatrixserverlib"
|
||||||
|
"github.com/matrix-org/gomatrixserverlib/spec"
|
||||||
"github.com/matrix-org/util"
|
"github.com/matrix-org/util"
|
||||||
|
|
||||||
"github.com/matrix-org/dendrite/clientapi/httputil"
|
"github.com/matrix-org/dendrite/clientapi/httputil"
|
||||||
"github.com/matrix-org/dendrite/clientapi/jsonerror"
|
|
||||||
"github.com/matrix-org/dendrite/internal/eventutil"
|
"github.com/matrix-org/dendrite/internal/eventutil"
|
||||||
"github.com/matrix-org/dendrite/internal/transactions"
|
"github.com/matrix-org/dendrite/internal/transactions"
|
||||||
roomserverAPI "github.com/matrix-org/dendrite/roomserver/api"
|
roomserverAPI "github.com/matrix-org/dendrite/roomserver/api"
|
||||||
|
"github.com/matrix-org/dendrite/roomserver/types"
|
||||||
"github.com/matrix-org/dendrite/setup/config"
|
"github.com/matrix-org/dendrite/setup/config"
|
||||||
userapi "github.com/matrix-org/dendrite/userapi/api"
|
userapi "github.com/matrix-org/dendrite/userapi/api"
|
||||||
)
|
)
|
||||||
|
|
||||||
type redactionContent struct {
|
type redactionContent struct {
|
||||||
Reason string `json:"reason"`
|
Reason string `json:"reason"`
|
||||||
|
Redacts string `json:"redacts"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type redactionResponse struct {
|
type redactionResponse struct {
|
||||||
|
@ -45,11 +48,43 @@ func SendRedaction(
|
||||||
txnID *string,
|
txnID *string,
|
||||||
txnCache *transactions.Cache,
|
txnCache *transactions.Cache,
|
||||||
) util.JSONResponse {
|
) util.JSONResponse {
|
||||||
resErr := checkMemberInRoom(req.Context(), rsAPI, device.UserID, roomID)
|
deviceUserID, userIDErr := spec.NewUserID(device.UserID, true)
|
||||||
|
if userIDErr != nil {
|
||||||
|
return util.JSONResponse{
|
||||||
|
Code: http.StatusForbidden,
|
||||||
|
JSON: spec.Forbidden("userID doesn't have power level to redact"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
validRoomID, err := spec.NewRoomID(roomID)
|
||||||
|
if err != nil {
|
||||||
|
return util.JSONResponse{
|
||||||
|
Code: http.StatusBadRequest,
|
||||||
|
JSON: spec.BadJSON("RoomID is invalid"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
senderID, queryErr := rsAPI.QuerySenderIDForUser(req.Context(), *validRoomID, *deviceUserID)
|
||||||
|
if queryErr != nil {
|
||||||
|
return util.JSONResponse{
|
||||||
|
Code: http.StatusForbidden,
|
||||||
|
JSON: spec.Forbidden("userID doesn't have power level to redact"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
resErr := checkMemberInRoom(req.Context(), rsAPI, *deviceUserID, roomID)
|
||||||
if resErr != nil {
|
if resErr != nil {
|
||||||
return *resErr
|
return *resErr
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// if user is member of room, and sender ID is nil, then this user doesn't have a pseudo ID for some reason,
|
||||||
|
// which is unexpected.
|
||||||
|
if senderID == nil {
|
||||||
|
util.GetLogger(req.Context()).WithField("userID", *deviceUserID).WithField("roomID", roomID).Error("missing sender ID for user, despite having membership")
|
||||||
|
return util.JSONResponse{
|
||||||
|
Code: http.StatusInternalServerError,
|
||||||
|
JSON: spec.Unknown("internal server error"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if txnID != nil {
|
if txnID != nil {
|
||||||
// Try to fetch response from transactionsCache
|
// Try to fetch response from transactionsCache
|
||||||
if res, ok := txnCache.FetchTransaction(device.AccessToken, *txnID, req.URL); ok {
|
if res, ok := txnCache.FetchTransaction(device.AccessToken, *txnID, req.URL); ok {
|
||||||
|
@ -61,46 +96,46 @@ func SendRedaction(
|
||||||
if ev == nil {
|
if ev == nil {
|
||||||
return util.JSONResponse{
|
return util.JSONResponse{
|
||||||
Code: 400,
|
Code: 400,
|
||||||
JSON: jsonerror.NotFound("unknown event ID"), // TODO: is it ok to leak existence?
|
JSON: spec.NotFound("unknown event ID"), // TODO: is it ok to leak existence?
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if ev.RoomID() != roomID {
|
if ev.RoomID().String() != roomID {
|
||||||
return util.JSONResponse{
|
return util.JSONResponse{
|
||||||
Code: 400,
|
Code: 400,
|
||||||
JSON: jsonerror.NotFound("cannot redact event in another room"),
|
JSON: spec.NotFound("cannot redact event in another room"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// "Users may redact their own events, and any user with a power level greater than or equal
|
// "Users may redact their own events, and any user with a power level greater than or equal
|
||||||
// to the redact power level of the room may redact events there"
|
// to the redact power level of the room may redact events there"
|
||||||
// https://matrix.org/docs/spec/client_server/r0.6.1#put-matrix-client-r0-rooms-roomid-redact-eventid-txnid
|
// https://matrix.org/docs/spec/client_server/r0.6.1#put-matrix-client-r0-rooms-roomid-redact-eventid-txnid
|
||||||
allowedToRedact := ev.Sender() == device.UserID
|
allowedToRedact := ev.SenderID() == *senderID
|
||||||
if !allowedToRedact {
|
if !allowedToRedact {
|
||||||
plEvent := roomserverAPI.GetStateEvent(req.Context(), rsAPI, roomID, gomatrixserverlib.StateKeyTuple{
|
plEvent := roomserverAPI.GetStateEvent(req.Context(), rsAPI, roomID, gomatrixserverlib.StateKeyTuple{
|
||||||
EventType: gomatrixserverlib.MRoomPowerLevels,
|
EventType: spec.MRoomPowerLevels,
|
||||||
StateKey: "",
|
StateKey: "",
|
||||||
})
|
})
|
||||||
if plEvent == nil {
|
if plEvent == nil {
|
||||||
return util.JSONResponse{
|
return util.JSONResponse{
|
||||||
Code: 403,
|
Code: 403,
|
||||||
JSON: jsonerror.Forbidden("You don't have permission to redact this event, no power_levels event in this room."),
|
JSON: spec.Forbidden("You don't have permission to redact this event, no power_levels event in this room."),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
pl, err := plEvent.PowerLevels()
|
pl, plErr := plEvent.PowerLevels()
|
||||||
if err != nil {
|
if plErr != nil {
|
||||||
return util.JSONResponse{
|
return util.JSONResponse{
|
||||||
Code: 403,
|
Code: 403,
|
||||||
JSON: jsonerror.Forbidden(
|
JSON: spec.Forbidden(
|
||||||
"You don't have permission to redact this event, the power_levels event for this room is malformed so auth checks cannot be performed.",
|
"You don't have permission to redact this event, the power_levels event for this room is malformed so auth checks cannot be performed.",
|
||||||
),
|
),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
allowedToRedact = pl.UserLevel(device.UserID) >= pl.Redact
|
allowedToRedact = pl.UserLevel(*senderID) >= pl.Redact
|
||||||
}
|
}
|
||||||
if !allowedToRedact {
|
if !allowedToRedact {
|
||||||
return util.JSONResponse{
|
return util.JSONResponse{
|
||||||
Code: 403,
|
Code: 403,
|
||||||
JSON: jsonerror.Forbidden("You don't have permission to redact this event, power level too low."),
|
JSON: spec.Forbidden("You don't have permission to redact this event, power level too low."),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -111,35 +146,49 @@ func SendRedaction(
|
||||||
}
|
}
|
||||||
|
|
||||||
// create the new event and set all the fields we can
|
// create the new event and set all the fields we can
|
||||||
builder := gomatrixserverlib.EventBuilder{
|
proto := gomatrixserverlib.ProtoEvent{
|
||||||
Sender: device.UserID,
|
SenderID: string(*senderID),
|
||||||
RoomID: roomID,
|
RoomID: roomID,
|
||||||
Type: gomatrixserverlib.MRoomRedaction,
|
Type: spec.MRoomRedaction,
|
||||||
Redacts: eventID,
|
Redacts: eventID,
|
||||||
}
|
|
||||||
err := builder.SetContent(r)
|
|
||||||
if err != nil {
|
|
||||||
util.GetLogger(req.Context()).WithError(err).Error("builder.SetContent failed")
|
|
||||||
return jsonerror.InternalServerError()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
identity, err := cfg.Matrix.SigningIdentityFor(device.UserDomain())
|
// Room version 11 expects the "redacts" field on the
|
||||||
|
// content field, so add it here as well
|
||||||
|
r.Redacts = eventID
|
||||||
|
|
||||||
|
err = proto.SetContent(r)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return jsonerror.InternalServerError()
|
util.GetLogger(req.Context()).WithError(err).Error("proto.SetContent failed")
|
||||||
|
return util.JSONResponse{
|
||||||
|
Code: http.StatusInternalServerError,
|
||||||
|
JSON: spec.InternalServerError{},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
identity, err := rsAPI.SigningIdentityFor(req.Context(), *validRoomID, *deviceUserID)
|
||||||
|
if err != nil {
|
||||||
|
return util.JSONResponse{
|
||||||
|
Code: http.StatusInternalServerError,
|
||||||
|
JSON: spec.InternalServerError{},
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var queryRes roomserverAPI.QueryLatestEventsAndStateResponse
|
var queryRes roomserverAPI.QueryLatestEventsAndStateResponse
|
||||||
e, err := eventutil.QueryAndBuildEvent(req.Context(), &builder, cfg.Matrix, identity, time.Now(), rsAPI, &queryRes)
|
e, err := eventutil.QueryAndBuildEvent(req.Context(), &proto, &identity, time.Now(), rsAPI, &queryRes)
|
||||||
if err == eventutil.ErrRoomNoExists {
|
if errors.Is(err, eventutil.ErrRoomNoExists{}) {
|
||||||
return util.JSONResponse{
|
return util.JSONResponse{
|
||||||
Code: http.StatusNotFound,
|
Code: http.StatusNotFound,
|
||||||
JSON: jsonerror.NotFound("Room does not exist"),
|
JSON: spec.NotFound("Room does not exist"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
domain := device.UserDomain()
|
domain := device.UserDomain()
|
||||||
if err = roomserverAPI.SendEvents(context.Background(), rsAPI, roomserverAPI.KindNew, []*gomatrixserverlib.HeaderedEvent{e}, device.UserDomain(), domain, domain, nil, false); err != nil {
|
if err = roomserverAPI.SendEvents(context.Background(), rsAPI, roomserverAPI.KindNew, []*types.HeaderedEvent{e}, device.UserDomain(), domain, domain, nil, false); err != nil {
|
||||||
util.GetLogger(req.Context()).WithError(err).Errorf("failed to SendEvents")
|
util.GetLogger(req.Context()).WithError(err).Errorf("failed to SendEvents")
|
||||||
return jsonerror.InternalServerError()
|
return util.JSONResponse{
|
||||||
|
Code: http.StatusInternalServerError,
|
||||||
|
JSON: spec.InternalServerError{},
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
res := util.JSONResponse{
|
res := util.JSONResponse{
|
||||||
|
|
|
@ -37,6 +37,7 @@ import (
|
||||||
"github.com/matrix-org/dendrite/setup/config"
|
"github.com/matrix-org/dendrite/setup/config"
|
||||||
|
|
||||||
"github.com/matrix-org/gomatrixserverlib"
|
"github.com/matrix-org/gomatrixserverlib"
|
||||||
|
"github.com/matrix-org/gomatrixserverlib/spec"
|
||||||
"github.com/matrix-org/gomatrixserverlib/tokens"
|
"github.com/matrix-org/gomatrixserverlib/tokens"
|
||||||
"github.com/matrix-org/util"
|
"github.com/matrix-org/util"
|
||||||
"github.com/prometheus/client_golang/prometheus"
|
"github.com/prometheus/client_golang/prometheus"
|
||||||
|
@ -45,7 +46,6 @@ import (
|
||||||
"github.com/matrix-org/dendrite/clientapi/auth"
|
"github.com/matrix-org/dendrite/clientapi/auth"
|
||||||
"github.com/matrix-org/dendrite/clientapi/auth/authtypes"
|
"github.com/matrix-org/dendrite/clientapi/auth/authtypes"
|
||||||
"github.com/matrix-org/dendrite/clientapi/httputil"
|
"github.com/matrix-org/dendrite/clientapi/httputil"
|
||||||
"github.com/matrix-org/dendrite/clientapi/jsonerror"
|
|
||||||
"github.com/matrix-org/dendrite/clientapi/userutil"
|
"github.com/matrix-org/dendrite/clientapi/userutil"
|
||||||
userapi "github.com/matrix-org/dendrite/userapi/api"
|
userapi "github.com/matrix-org/dendrite/userapi/api"
|
||||||
)
|
)
|
||||||
|
@ -164,7 +164,7 @@ func (d *sessionsDict) addCompletedSessionStage(sessionID string, stage authtype
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
d.sessions[sessionID] = append(sessions.sessions[sessionID], stage)
|
d.sessions[sessionID] = append(d.sessions[sessionID], stage)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *sessionsDict) addDeviceToDelete(sessionID, deviceID string) {
|
func (d *sessionsDict) addDeviceToDelete(sessionID, deviceID string) {
|
||||||
|
@ -206,10 +206,10 @@ var (
|
||||||
// previous parameters with the ones supplied. This mean you cannot "build up" request params.
|
// previous parameters with the ones supplied. This mean you cannot "build up" request params.
|
||||||
type registerRequest struct {
|
type registerRequest struct {
|
||||||
// registration parameters
|
// registration parameters
|
||||||
Password string `json:"password"`
|
Password string `json:"password"`
|
||||||
Username string `json:"username"`
|
Username string `json:"username"`
|
||||||
ServerName gomatrixserverlib.ServerName `json:"-"`
|
ServerName spec.ServerName `json:"-"`
|
||||||
Admin bool `json:"admin"`
|
Admin bool `json:"admin"`
|
||||||
// user-interactive auth params
|
// user-interactive auth params
|
||||||
Auth authDict `json:"auth"`
|
Auth authDict `json:"auth"`
|
||||||
|
|
||||||
|
@ -236,7 +236,7 @@ type authDict struct {
|
||||||
// TODO: Lots of custom keys depending on the type
|
// TODO: Lots of custom keys depending on the type
|
||||||
}
|
}
|
||||||
|
|
||||||
// http://matrix.org/speculator/spec/HEAD/client_server/unstable.html#user-interactive-authentication-api
|
// https://spec.matrix.org/v1.7/client-server-api/#user-interactive-authentication-api
|
||||||
type userInteractiveResponse struct {
|
type userInteractiveResponse struct {
|
||||||
Flows []authtypes.Flow `json:"flows"`
|
Flows []authtypes.Flow `json:"flows"`
|
||||||
Completed []authtypes.LoginType `json:"completed"`
|
Completed []authtypes.LoginType `json:"completed"`
|
||||||
|
@ -256,7 +256,7 @@ func newUserInteractiveResponse(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// http://matrix.org/speculator/spec/HEAD/client_server/unstable.html#post-matrix-client-unstable-register
|
// https://spec.matrix.org/v1.7/client-server-api/#post_matrixclientv3register
|
||||||
type registerResponse struct {
|
type registerResponse struct {
|
||||||
UserID string `json:"user_id"`
|
UserID string `json:"user_id"`
|
||||||
AccessToken string `json:"access_token,omitempty"`
|
AccessToken string `json:"access_token,omitempty"`
|
||||||
|
@ -427,7 +427,7 @@ func validateApplicationService(
|
||||||
if matchedApplicationService == nil {
|
if matchedApplicationService == nil {
|
||||||
return "", &util.JSONResponse{
|
return "", &util.JSONResponse{
|
||||||
Code: http.StatusUnauthorized,
|
Code: http.StatusUnauthorized,
|
||||||
JSON: jsonerror.UnknownToken("Supplied access_token does not match any known application service"),
|
JSON: spec.UnknownToken("Supplied access_token does not match any known application service"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -438,7 +438,7 @@ func validateApplicationService(
|
||||||
// If we didn't find any matches, return M_EXCLUSIVE
|
// If we didn't find any matches, return M_EXCLUSIVE
|
||||||
return "", &util.JSONResponse{
|
return "", &util.JSONResponse{
|
||||||
Code: http.StatusBadRequest,
|
Code: http.StatusBadRequest,
|
||||||
JSON: jsonerror.ASExclusive(fmt.Sprintf(
|
JSON: spec.ASExclusive(fmt.Sprintf(
|
||||||
"Supplied username %s did not match any namespaces for application service ID: %s", username, matchedApplicationService.ID)),
|
"Supplied username %s did not match any namespaces for application service ID: %s", username, matchedApplicationService.ID)),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -447,7 +447,7 @@ func validateApplicationService(
|
||||||
if UsernameMatchesMultipleExclusiveNamespaces(cfg, userID) {
|
if UsernameMatchesMultipleExclusiveNamespaces(cfg, userID) {
|
||||||
return "", &util.JSONResponse{
|
return "", &util.JSONResponse{
|
||||||
Code: http.StatusBadRequest,
|
Code: http.StatusBadRequest,
|
||||||
JSON: jsonerror.ASExclusive(fmt.Sprintf(
|
JSON: spec.ASExclusive(fmt.Sprintf(
|
||||||
"Supplied username %s matches multiple exclusive application service namespaces. Only 1 match allowed", username)),
|
"Supplied username %s matches multiple exclusive application service namespaces. Only 1 match allowed", username)),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -462,7 +462,7 @@ func validateApplicationService(
|
||||||
}
|
}
|
||||||
|
|
||||||
// Register processes a /register request.
|
// Register processes a /register request.
|
||||||
// http://matrix.org/speculator/spec/HEAD/client_server/unstable.html#post-matrix-client-unstable-register
|
// https://spec.matrix.org/v1.7/client-server-api/#post_matrixclientv3register
|
||||||
func Register(
|
func Register(
|
||||||
req *http.Request,
|
req *http.Request,
|
||||||
userAPI userapi.ClientUserAPI,
|
userAPI userapi.ClientUserAPI,
|
||||||
|
@ -473,12 +473,12 @@ func Register(
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return util.JSONResponse{
|
return util.JSONResponse{
|
||||||
Code: http.StatusBadRequest,
|
Code: http.StatusBadRequest,
|
||||||
JSON: jsonerror.NotJSON("Unable to read request body"),
|
JSON: spec.NotJSON("Unable to read request body"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var r registerRequest
|
var r registerRequest
|
||||||
host := gomatrixserverlib.ServerName(req.Host)
|
host := spec.ServerName(req.Host)
|
||||||
if v := cfg.Matrix.VirtualHostForHTTPHost(host); v != nil {
|
if v := cfg.Matrix.VirtualHostForHTTPHost(host); v != nil {
|
||||||
r.ServerName = v.ServerName
|
r.ServerName = v.ServerName
|
||||||
} else {
|
} else {
|
||||||
|
@ -517,7 +517,7 @@ func Register(
|
||||||
if _, err = strconv.ParseInt(r.Username, 10, 64); err == nil {
|
if _, err = strconv.ParseInt(r.Username, 10, 64); err == nil {
|
||||||
return util.JSONResponse{
|
return util.JSONResponse{
|
||||||
Code: http.StatusBadRequest,
|
Code: http.StatusBadRequest,
|
||||||
JSON: jsonerror.InvalidUsername("Numeric user IDs are reserved"),
|
JSON: spec.InvalidUsername("Numeric user IDs are reserved"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Auto generate a numeric username if r.Username is empty
|
// Auto generate a numeric username if r.Username is empty
|
||||||
|
@ -528,7 +528,10 @@ func Register(
|
||||||
nres := &userapi.QueryNumericLocalpartResponse{}
|
nres := &userapi.QueryNumericLocalpartResponse{}
|
||||||
if err = userAPI.QueryNumericLocalpart(req.Context(), nreq, nres); err != nil {
|
if err = userAPI.QueryNumericLocalpart(req.Context(), nreq, nres); err != nil {
|
||||||
util.GetLogger(req.Context()).WithError(err).Error("userAPI.QueryNumericLocalpart failed")
|
util.GetLogger(req.Context()).WithError(err).Error("userAPI.QueryNumericLocalpart failed")
|
||||||
return jsonerror.InternalServerError()
|
return util.JSONResponse{
|
||||||
|
Code: http.StatusInternalServerError,
|
||||||
|
JSON: spec.InternalServerError{},
|
||||||
|
}
|
||||||
}
|
}
|
||||||
r.Username = strconv.FormatInt(nres.ID, 10)
|
r.Username = strconv.FormatInt(nres.ID, 10)
|
||||||
}
|
}
|
||||||
|
@ -551,7 +554,7 @@ func Register(
|
||||||
// type is not known or specified)
|
// type is not known or specified)
|
||||||
return util.JSONResponse{
|
return util.JSONResponse{
|
||||||
Code: http.StatusBadRequest,
|
Code: http.StatusBadRequest,
|
||||||
JSON: jsonerror.MissingArgument("A known registration type (e.g. m.login.application_service) must be specified if an access_token is provided"),
|
JSON: spec.MissingParam("A known registration type (e.g. m.login.application_service) must be specified if an access_token is provided"),
|
||||||
}
|
}
|
||||||
default:
|
default:
|
||||||
// Spec-compliant case (neither the access_token nor the login type are
|
// Spec-compliant case (neither the access_token nor the login type are
|
||||||
|
@ -589,7 +592,7 @@ func handleGuestRegistration(
|
||||||
if !registrationEnabled || !guestsEnabled {
|
if !registrationEnabled || !guestsEnabled {
|
||||||
return util.JSONResponse{
|
return util.JSONResponse{
|
||||||
Code: http.StatusForbidden,
|
Code: http.StatusForbidden,
|
||||||
JSON: jsonerror.Forbidden(
|
JSON: spec.Forbidden(
|
||||||
fmt.Sprintf("Guest registration is disabled on %q", r.ServerName),
|
fmt.Sprintf("Guest registration is disabled on %q", r.ServerName),
|
||||||
),
|
),
|
||||||
}
|
}
|
||||||
|
@ -603,7 +606,7 @@ func handleGuestRegistration(
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return util.JSONResponse{
|
return util.JSONResponse{
|
||||||
Code: http.StatusInternalServerError,
|
Code: http.StatusInternalServerError,
|
||||||
JSON: jsonerror.Unknown("failed to create account: " + err.Error()),
|
JSON: spec.Unknown("failed to create account: " + err.Error()),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
token, err := tokens.GenerateLoginToken(tokens.TokenOptions{
|
token, err := tokens.GenerateLoginToken(tokens.TokenOptions{
|
||||||
|
@ -615,7 +618,7 @@ func handleGuestRegistration(
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return util.JSONResponse{
|
return util.JSONResponse{
|
||||||
Code: http.StatusInternalServerError,
|
Code: http.StatusInternalServerError,
|
||||||
JSON: jsonerror.Unknown("Failed to generate access token"),
|
JSON: spec.Unknown("Failed to generate access token"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
//we don't allow guests to specify their own device_id
|
//we don't allow guests to specify their own device_id
|
||||||
|
@ -627,11 +630,12 @@ func handleGuestRegistration(
|
||||||
AccessToken: token,
|
AccessToken: token,
|
||||||
IPAddr: req.RemoteAddr,
|
IPAddr: req.RemoteAddr,
|
||||||
UserAgent: req.UserAgent(),
|
UserAgent: req.UserAgent(),
|
||||||
|
FromRegistration: true,
|
||||||
}, &devRes)
|
}, &devRes)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return util.JSONResponse{
|
return util.JSONResponse{
|
||||||
Code: http.StatusInternalServerError,
|
Code: http.StatusInternalServerError,
|
||||||
JSON: jsonerror.Unknown("failed to create device: " + err.Error()),
|
JSON: spec.Unknown("failed to create device: " + err.Error()),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return util.JSONResponse{
|
return util.JSONResponse{
|
||||||
|
@ -644,6 +648,16 @@ func handleGuestRegistration(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// localpartMatchesExclusiveNamespaces will check if a given username matches any
|
||||||
|
// application service's exclusive users namespace
|
||||||
|
func localpartMatchesExclusiveNamespaces(
|
||||||
|
cfg *config.ClientAPI,
|
||||||
|
localpart string,
|
||||||
|
) bool {
|
||||||
|
userID := userutil.MakeUserID(localpart, cfg.Matrix.ServerName)
|
||||||
|
return cfg.Derived.ExclusiveApplicationServicesUsernameRegexp.MatchString(userID)
|
||||||
|
}
|
||||||
|
|
||||||
// handleRegistrationFlow will direct and complete registration flow stages
|
// handleRegistrationFlow will direct and complete registration flow stages
|
||||||
// that the client has requested.
|
// that the client has requested.
|
||||||
// nolint: gocyclo
|
// nolint: gocyclo
|
||||||
|
@ -681,7 +695,7 @@ func handleRegistrationFlow(
|
||||||
if !registrationEnabled && r.Auth.Type != authtypes.LoginTypeSharedSecret {
|
if !registrationEnabled && r.Auth.Type != authtypes.LoginTypeSharedSecret {
|
||||||
return util.JSONResponse{
|
return util.JSONResponse{
|
||||||
Code: http.StatusForbidden,
|
Code: http.StatusForbidden,
|
||||||
JSON: jsonerror.Forbidden(
|
JSON: spec.Forbidden(
|
||||||
fmt.Sprintf("Registration is disabled on %q", r.ServerName),
|
fmt.Sprintf("Registration is disabled on %q", r.ServerName),
|
||||||
),
|
),
|
||||||
}
|
}
|
||||||
|
@ -692,10 +706,10 @@ func handleRegistrationFlow(
|
||||||
// If an access token is provided, ignore this check this is an appservice
|
// If an access token is provided, ignore this check this is an appservice
|
||||||
// request and we will validate in validateApplicationService
|
// request and we will validate in validateApplicationService
|
||||||
if len(cfg.Derived.ApplicationServices) != 0 &&
|
if len(cfg.Derived.ApplicationServices) != 0 &&
|
||||||
UsernameMatchesExclusiveNamespaces(cfg, r.Username) {
|
localpartMatchesExclusiveNamespaces(cfg, r.Username) {
|
||||||
return util.JSONResponse{
|
return util.JSONResponse{
|
||||||
Code: http.StatusBadRequest,
|
Code: http.StatusBadRequest,
|
||||||
JSON: jsonerror.ASExclusive("This username is reserved by an application service."),
|
JSON: spec.ASExclusive("This username is reserved by an application service."),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -705,15 +719,15 @@ func handleRegistrationFlow(
|
||||||
err := validateRecaptcha(cfg, r.Auth.Response, req.RemoteAddr)
|
err := validateRecaptcha(cfg, r.Auth.Response, req.RemoteAddr)
|
||||||
switch err {
|
switch err {
|
||||||
case ErrCaptchaDisabled:
|
case ErrCaptchaDisabled:
|
||||||
return util.JSONResponse{Code: http.StatusForbidden, JSON: jsonerror.Unknown(err.Error())}
|
return util.JSONResponse{Code: http.StatusForbidden, JSON: spec.Unknown(err.Error())}
|
||||||
case ErrMissingResponse:
|
case ErrMissingResponse:
|
||||||
return util.JSONResponse{Code: http.StatusBadRequest, JSON: jsonerror.BadJSON(err.Error())}
|
return util.JSONResponse{Code: http.StatusBadRequest, JSON: spec.BadJSON(err.Error())}
|
||||||
case ErrInvalidCaptcha:
|
case ErrInvalidCaptcha:
|
||||||
return util.JSONResponse{Code: http.StatusUnauthorized, JSON: jsonerror.BadJSON(err.Error())}
|
return util.JSONResponse{Code: http.StatusUnauthorized, JSON: spec.BadJSON(err.Error())}
|
||||||
case nil:
|
case nil:
|
||||||
default:
|
default:
|
||||||
util.GetLogger(req.Context()).WithError(err).Error("failed to validate recaptcha")
|
util.GetLogger(req.Context()).WithError(err).Error("failed to validate recaptcha")
|
||||||
return util.JSONResponse{Code: http.StatusInternalServerError, JSON: jsonerror.InternalServerError()}
|
return util.JSONResponse{Code: http.StatusInternalServerError, JSON: spec.InternalServerError{}}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add Recaptcha to the list of completed registration stages
|
// Add Recaptcha to the list of completed registration stages
|
||||||
|
@ -731,7 +745,7 @@ func handleRegistrationFlow(
|
||||||
default:
|
default:
|
||||||
return util.JSONResponse{
|
return util.JSONResponse{
|
||||||
Code: http.StatusNotImplemented,
|
Code: http.StatusNotImplemented,
|
||||||
JSON: jsonerror.Unknown("unknown/unimplemented auth type"),
|
JSON: spec.Unknown("unknown/unimplemented auth type"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -763,13 +777,13 @@ func handleApplicationServiceRegistration(
|
||||||
if tokenErr != nil {
|
if tokenErr != nil {
|
||||||
return util.JSONResponse{
|
return util.JSONResponse{
|
||||||
Code: http.StatusUnauthorized,
|
Code: http.StatusUnauthorized,
|
||||||
JSON: jsonerror.MissingToken(tokenErr.Error()),
|
JSON: spec.MissingToken(tokenErr.Error()),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check application service register user request is valid.
|
// Check application service register user request is valid.
|
||||||
// The application service's ID is returned if so.
|
// The application service's ID is returned if so.
|
||||||
appserviceID, err := validateApplicationService(
|
appserviceID, err := internal.ValidateApplicationServiceRequest(
|
||||||
cfg, r.Username, accessToken,
|
cfg, r.Username, accessToken,
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -824,7 +838,7 @@ func checkAndCompleteFlow(
|
||||||
func completeRegistration(
|
func completeRegistration(
|
||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
userAPI userapi.ClientUserAPI,
|
userAPI userapi.ClientUserAPI,
|
||||||
username string, serverName gomatrixserverlib.ServerName, displayName string,
|
username string, serverName spec.ServerName, displayName string,
|
||||||
password, appserviceID, ipAddr, userAgent, sessionID string,
|
password, appserviceID, ipAddr, userAgent, sessionID string,
|
||||||
inhibitLogin eventutil.WeakBoolean,
|
inhibitLogin eventutil.WeakBoolean,
|
||||||
deviceDisplayName, deviceID *string,
|
deviceDisplayName, deviceID *string,
|
||||||
|
@ -833,14 +847,14 @@ func completeRegistration(
|
||||||
if username == "" {
|
if username == "" {
|
||||||
return util.JSONResponse{
|
return util.JSONResponse{
|
||||||
Code: http.StatusBadRequest,
|
Code: http.StatusBadRequest,
|
||||||
JSON: jsonerror.MissingArgument("Missing username"),
|
JSON: spec.MissingParam("Missing username"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Blank passwords are only allowed by registered application services
|
// Blank passwords are only allowed by registered application services
|
||||||
if password == "" && appserviceID == "" {
|
if password == "" && appserviceID == "" {
|
||||||
return util.JSONResponse{
|
return util.JSONResponse{
|
||||||
Code: http.StatusBadRequest,
|
Code: http.StatusBadRequest,
|
||||||
JSON: jsonerror.MissingArgument("Missing password"),
|
JSON: spec.MissingParam("Missing password"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
var accRes userapi.PerformAccountCreationResponse
|
var accRes userapi.PerformAccountCreationResponse
|
||||||
|
@ -856,12 +870,12 @@ func completeRegistration(
|
||||||
if _, ok := err.(*userapi.ErrorConflict); ok { // user already exists
|
if _, ok := err.(*userapi.ErrorConflict); ok { // user already exists
|
||||||
return util.JSONResponse{
|
return util.JSONResponse{
|
||||||
Code: http.StatusBadRequest,
|
Code: http.StatusBadRequest,
|
||||||
JSON: jsonerror.UserInUse("Desired user ID is already taken."),
|
JSON: spec.UserInUse("Desired user ID is already taken."),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return util.JSONResponse{
|
return util.JSONResponse{
|
||||||
Code: http.StatusInternalServerError,
|
Code: http.StatusInternalServerError,
|
||||||
JSON: jsonerror.Unknown("failed to create account: " + err.Error()),
|
JSON: spec.Unknown("failed to create account: " + err.Error()),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -883,22 +897,16 @@ func completeRegistration(
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return util.JSONResponse{
|
return util.JSONResponse{
|
||||||
Code: http.StatusInternalServerError,
|
Code: http.StatusInternalServerError,
|
||||||
JSON: jsonerror.Unknown("Failed to generate access token"),
|
JSON: spec.Unknown("Failed to generate access token"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if displayName != "" {
|
if displayName != "" {
|
||||||
nameReq := userapi.PerformUpdateDisplayNameRequest{
|
_, _, err = userAPI.SetDisplayName(ctx, username, serverName, displayName)
|
||||||
Localpart: username,
|
|
||||||
ServerName: serverName,
|
|
||||||
DisplayName: displayName,
|
|
||||||
}
|
|
||||||
var nameRes userapi.PerformUpdateDisplayNameResponse
|
|
||||||
err = userAPI.SetDisplayName(ctx, &nameReq, &nameRes)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return util.JSONResponse{
|
return util.JSONResponse{
|
||||||
Code: http.StatusInternalServerError,
|
Code: http.StatusInternalServerError,
|
||||||
JSON: jsonerror.Unknown("failed to set display name: " + err.Error()),
|
JSON: spec.Unknown("failed to set display name: " + err.Error()),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -912,11 +920,12 @@ func completeRegistration(
|
||||||
DeviceID: deviceID,
|
DeviceID: deviceID,
|
||||||
IPAddr: ipAddr,
|
IPAddr: ipAddr,
|
||||||
UserAgent: userAgent,
|
UserAgent: userAgent,
|
||||||
|
FromRegistration: true,
|
||||||
}, &devRes)
|
}, &devRes)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return util.JSONResponse{
|
return util.JSONResponse{
|
||||||
Code: http.StatusInternalServerError,
|
Code: http.StatusInternalServerError,
|
||||||
JSON: jsonerror.Unknown("failed to create device: " + err.Error()),
|
JSON: spec.Unknown("failed to create device: " + err.Error()),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1000,7 +1009,7 @@ func RegisterAvailable(
|
||||||
// Squash username to all lowercase letters
|
// Squash username to all lowercase letters
|
||||||
username = strings.ToLower(username)
|
username = strings.ToLower(username)
|
||||||
domain := cfg.Matrix.ServerName
|
domain := cfg.Matrix.ServerName
|
||||||
host := gomatrixserverlib.ServerName(req.Host)
|
host := spec.ServerName(req.Host)
|
||||||
if v := cfg.Matrix.VirtualHostForHTTPHost(host); v != nil {
|
if v := cfg.Matrix.VirtualHostForHTTPHost(host); v != nil {
|
||||||
domain = v.ServerName
|
domain = v.ServerName
|
||||||
}
|
}
|
||||||
|
@ -1011,7 +1020,7 @@ func RegisterAvailable(
|
||||||
if v.ServerName == domain && !v.AllowRegistration {
|
if v.ServerName == domain && !v.AllowRegistration {
|
||||||
return util.JSONResponse{
|
return util.JSONResponse{
|
||||||
Code: http.StatusForbidden,
|
Code: http.StatusForbidden,
|
||||||
JSON: jsonerror.Forbidden(
|
JSON: spec.Forbidden(
|
||||||
fmt.Sprintf("Registration is not allowed on %q", string(v.ServerName)),
|
fmt.Sprintf("Registration is not allowed on %q", string(v.ServerName)),
|
||||||
),
|
),
|
||||||
}
|
}
|
||||||
|
@ -1028,7 +1037,7 @@ func RegisterAvailable(
|
||||||
if appservice.OwnsNamespaceCoveringUserId(userID) {
|
if appservice.OwnsNamespaceCoveringUserId(userID) {
|
||||||
return util.JSONResponse{
|
return util.JSONResponse{
|
||||||
Code: http.StatusBadRequest,
|
Code: http.StatusBadRequest,
|
||||||
JSON: jsonerror.UserInUse("Desired user ID is reserved by an application service."),
|
JSON: spec.UserInUse("Desired user ID is reserved by an application service."),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1041,14 +1050,14 @@ func RegisterAvailable(
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return util.JSONResponse{
|
return util.JSONResponse{
|
||||||
Code: http.StatusInternalServerError,
|
Code: http.StatusInternalServerError,
|
||||||
JSON: jsonerror.Unknown("failed to check availability:" + err.Error()),
|
JSON: spec.Unknown("failed to check availability:" + err.Error()),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if !res.Available {
|
if !res.Available {
|
||||||
return util.JSONResponse{
|
return util.JSONResponse{
|
||||||
Code: http.StatusBadRequest,
|
Code: http.StatusBadRequest,
|
||||||
JSON: jsonerror.UserInUse("Desired User ID is already taken."),
|
JSON: spec.UserInUse("Desired User ID is already taken."),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1065,7 +1074,7 @@ func handleSharedSecretRegistration(cfg *config.ClientAPI, userAPI userapi.Clien
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return util.JSONResponse{
|
return util.JSONResponse{
|
||||||
Code: 400,
|
Code: 400,
|
||||||
JSON: jsonerror.BadJSON(fmt.Sprintf("malformed json: %s", err)),
|
JSON: spec.BadJSON(fmt.Sprintf("malformed json: %s", err)),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
valid, err := sr.IsValidMacLogin(ssrr.Nonce, ssrr.User, ssrr.Password, ssrr.Admin, ssrr.MacBytes)
|
valid, err := sr.IsValidMacLogin(ssrr.Nonce, ssrr.User, ssrr.Password, ssrr.Admin, ssrr.MacBytes)
|
||||||
|
@ -1075,7 +1084,7 @@ func handleSharedSecretRegistration(cfg *config.ClientAPI, userAPI userapi.Clien
|
||||||
if !valid {
|
if !valid {
|
||||||
return util.JSONResponse{
|
return util.JSONResponse{
|
||||||
Code: 403,
|
Code: 403,
|
||||||
JSON: jsonerror.Forbidden("bad mac"),
|
JSON: spec.Forbidden("bad mac"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// downcase capitals
|
// downcase capitals
|
||||||
|
|
|
@ -28,14 +28,17 @@ import (
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/matrix-org/dendrite/clientapi/auth/authtypes"
|
"github.com/matrix-org/dendrite/clientapi/auth/authtypes"
|
||||||
"github.com/matrix-org/dendrite/clientapi/jsonerror"
|
|
||||||
"github.com/matrix-org/dendrite/internal"
|
"github.com/matrix-org/dendrite/internal"
|
||||||
|
"github.com/matrix-org/dendrite/internal/caching"
|
||||||
|
"github.com/matrix-org/dendrite/internal/sqlutil"
|
||||||
"github.com/matrix-org/dendrite/roomserver"
|
"github.com/matrix-org/dendrite/roomserver"
|
||||||
"github.com/matrix-org/dendrite/setup/config"
|
"github.com/matrix-org/dendrite/setup/config"
|
||||||
|
"github.com/matrix-org/dendrite/setup/jetstream"
|
||||||
"github.com/matrix-org/dendrite/test"
|
"github.com/matrix-org/dendrite/test"
|
||||||
"github.com/matrix-org/dendrite/test/testrig"
|
"github.com/matrix-org/dendrite/test/testrig"
|
||||||
"github.com/matrix-org/dendrite/userapi"
|
"github.com/matrix-org/dendrite/userapi"
|
||||||
"github.com/matrix-org/dendrite/userapi/api"
|
"github.com/matrix-org/dendrite/userapi/api"
|
||||||
|
"github.com/matrix-org/gomatrixserverlib/spec"
|
||||||
"github.com/matrix-org/util"
|
"github.com/matrix-org/util"
|
||||||
"github.com/patrickmn/go-cache"
|
"github.com/patrickmn/go-cache"
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
|
@ -295,53 +298,65 @@ func Test_register(t *testing.T) {
|
||||||
guestsDisabled bool
|
guestsDisabled bool
|
||||||
enableRecaptcha bool
|
enableRecaptcha bool
|
||||||
captchaBody string
|
captchaBody string
|
||||||
wantResponse util.JSONResponse
|
// in case of an error, the expected response
|
||||||
|
wantErrorResponse util.JSONResponse
|
||||||
|
// in case of success, the expected username assigned
|
||||||
|
wantUsername string
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
name: "disallow guests",
|
name: "disallow guests",
|
||||||
kind: "guest",
|
kind: "guest",
|
||||||
guestsDisabled: true,
|
guestsDisabled: true,
|
||||||
wantResponse: util.JSONResponse{
|
wantErrorResponse: util.JSONResponse{
|
||||||
Code: http.StatusForbidden,
|
Code: http.StatusForbidden,
|
||||||
JSON: jsonerror.Forbidden(`Guest registration is disabled on "test"`),
|
JSON: spec.Forbidden(`Guest registration is disabled on "test"`),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "allow guests",
|
name: "allow guests",
|
||||||
kind: "guest",
|
kind: "guest",
|
||||||
|
wantUsername: "1",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "unknown login type",
|
name: "unknown login type",
|
||||||
loginType: "im.not.known",
|
loginType: "im.not.known",
|
||||||
wantResponse: util.JSONResponse{
|
wantErrorResponse: util.JSONResponse{
|
||||||
Code: http.StatusNotImplemented,
|
Code: http.StatusNotImplemented,
|
||||||
JSON: jsonerror.Unknown("unknown/unimplemented auth type"),
|
JSON: spec.Unknown("unknown/unimplemented auth type"),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "disabled registration",
|
name: "disabled registration",
|
||||||
registrationDisabled: true,
|
registrationDisabled: true,
|
||||||
wantResponse: util.JSONResponse{
|
wantErrorResponse: util.JSONResponse{
|
||||||
Code: http.StatusForbidden,
|
Code: http.StatusForbidden,
|
||||||
JSON: jsonerror.Forbidden(`Registration is disabled on "test"`),
|
JSON: spec.Forbidden(`Registration is disabled on "test"`),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "successful registration, numeric ID",
|
name: "successful registration, numeric ID",
|
||||||
username: "",
|
username: "",
|
||||||
password: "someRandomPassword",
|
password: "someRandomPassword",
|
||||||
forceEmpty: true,
|
forceEmpty: true,
|
||||||
|
wantUsername: "2",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "successful registration",
|
name: "successful registration",
|
||||||
username: "success",
|
username: "success",
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: "successful registration, sequential numeric ID",
|
||||||
|
username: "",
|
||||||
|
password: "someRandomPassword",
|
||||||
|
forceEmpty: true,
|
||||||
|
wantUsername: "3",
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: "failing registration - user already exists",
|
name: "failing registration - user already exists",
|
||||||
username: "success",
|
username: "success",
|
||||||
wantResponse: util.JSONResponse{
|
wantErrorResponse: util.JSONResponse{
|
||||||
Code: http.StatusBadRequest,
|
Code: http.StatusBadRequest,
|
||||||
JSON: jsonerror.UserInUse("Desired user ID is already taken."),
|
JSON: spec.UserInUse("Desired user ID is already taken."),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
@ -349,33 +364,33 @@ func Test_register(t *testing.T) {
|
||||||
username: "LOWERCASED", // this is going to be lower-cased
|
username: "LOWERCASED", // this is going to be lower-cased
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "invalid username",
|
name: "invalid username",
|
||||||
username: "#totalyNotValid",
|
username: "#totalyNotValid",
|
||||||
wantResponse: *internal.UsernameResponse(internal.ErrUsernameInvalid),
|
wantErrorResponse: *internal.UsernameResponse(internal.ErrUsernameInvalid),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "numeric username is forbidden",
|
name: "numeric username is forbidden",
|
||||||
username: "1337",
|
username: "1337",
|
||||||
wantResponse: util.JSONResponse{
|
wantErrorResponse: util.JSONResponse{
|
||||||
Code: http.StatusBadRequest,
|
Code: http.StatusBadRequest,
|
||||||
JSON: jsonerror.InvalidUsername("Numeric user IDs are reserved"),
|
JSON: spec.InvalidUsername("Numeric user IDs are reserved"),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "disabled recaptcha login",
|
name: "disabled recaptcha login",
|
||||||
loginType: authtypes.LoginTypeRecaptcha,
|
loginType: authtypes.LoginTypeRecaptcha,
|
||||||
wantResponse: util.JSONResponse{
|
wantErrorResponse: util.JSONResponse{
|
||||||
Code: http.StatusForbidden,
|
Code: http.StatusForbidden,
|
||||||
JSON: jsonerror.Unknown(ErrCaptchaDisabled.Error()),
|
JSON: spec.Unknown(ErrCaptchaDisabled.Error()),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "enabled recaptcha, no response defined",
|
name: "enabled recaptcha, no response defined",
|
||||||
enableRecaptcha: true,
|
enableRecaptcha: true,
|
||||||
loginType: authtypes.LoginTypeRecaptcha,
|
loginType: authtypes.LoginTypeRecaptcha,
|
||||||
wantResponse: util.JSONResponse{
|
wantErrorResponse: util.JSONResponse{
|
||||||
Code: http.StatusBadRequest,
|
Code: http.StatusBadRequest,
|
||||||
JSON: jsonerror.BadJSON(ErrMissingResponse.Error()),
|
JSON: spec.BadJSON(ErrMissingResponse.Error()),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
@ -383,9 +398,9 @@ func Test_register(t *testing.T) {
|
||||||
enableRecaptcha: true,
|
enableRecaptcha: true,
|
||||||
loginType: authtypes.LoginTypeRecaptcha,
|
loginType: authtypes.LoginTypeRecaptcha,
|
||||||
captchaBody: `notvalid`,
|
captchaBody: `notvalid`,
|
||||||
wantResponse: util.JSONResponse{
|
wantErrorResponse: util.JSONResponse{
|
||||||
Code: http.StatusUnauthorized,
|
Code: http.StatusUnauthorized,
|
||||||
JSON: jsonerror.BadJSON(ErrInvalidCaptcha.Error()),
|
JSON: spec.BadJSON(ErrInvalidCaptcha.Error()),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
@ -395,20 +410,25 @@ func Test_register(t *testing.T) {
|
||||||
captchaBody: `success`,
|
captchaBody: `success`,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "captcha invalid from remote",
|
name: "captcha invalid from remote",
|
||||||
enableRecaptcha: true,
|
enableRecaptcha: true,
|
||||||
loginType: authtypes.LoginTypeRecaptcha,
|
loginType: authtypes.LoginTypeRecaptcha,
|
||||||
captchaBody: `i should fail for other reasons`,
|
captchaBody: `i should fail for other reasons`,
|
||||||
wantResponse: util.JSONResponse{Code: http.StatusInternalServerError, JSON: jsonerror.InternalServerError()},
|
wantErrorResponse: util.JSONResponse{Code: http.StatusInternalServerError, JSON: spec.InternalServerError{}},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
test.WithAllDatabases(t, func(t *testing.T, dbType test.DBType) {
|
test.WithAllDatabases(t, func(t *testing.T, dbType test.DBType) {
|
||||||
base, baseClose := testrig.CreateBaseDendrite(t, dbType)
|
cfg, processCtx, close := testrig.CreateConfig(t, dbType)
|
||||||
defer baseClose()
|
defer close()
|
||||||
|
|
||||||
rsAPI := roomserver.NewInternalAPI(base)
|
caches := caching.NewRistrettoCache(128*1024*1024, time.Hour, caching.DisableMetrics)
|
||||||
userAPI := userapi.NewInternalAPI(base, rsAPI, nil)
|
natsInstance := jetstream.NATSInstance{}
|
||||||
|
|
||||||
|
cm := sqlutil.NewConnectionManager(processCtx, cfg.Global.DatabaseOptions)
|
||||||
|
rsAPI := roomserver.NewInternalAPI(processCtx, cfg, cm, &natsInstance, caches, caching.DisableMetrics)
|
||||||
|
rsAPI.SetFederationAPI(nil, nil)
|
||||||
|
userAPI := userapi.NewInternalAPI(processCtx, cfg, cm, &natsInstance, rsAPI, nil, caching.DisableMetrics, testIsBlacklistedOrBackingOff)
|
||||||
|
|
||||||
for _, tc := range testCases {
|
for _, tc := range testCases {
|
||||||
t.Run(tc.name, func(t *testing.T) {
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
@ -430,16 +450,16 @@ func Test_register(t *testing.T) {
|
||||||
}
|
}
|
||||||
}))
|
}))
|
||||||
defer srv.Close()
|
defer srv.Close()
|
||||||
base.Cfg.ClientAPI.RecaptchaSiteVerifyAPI = srv.URL
|
cfg.ClientAPI.RecaptchaSiteVerifyAPI = srv.URL
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := base.Cfg.Derive(); err != nil {
|
if err := cfg.Derive(); err != nil {
|
||||||
t.Fatalf("failed to derive config: %s", err)
|
t.Fatalf("failed to derive config: %s", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
base.Cfg.ClientAPI.RecaptchaEnabled = tc.enableRecaptcha
|
cfg.ClientAPI.RecaptchaEnabled = tc.enableRecaptcha
|
||||||
base.Cfg.ClientAPI.RegistrationDisabled = tc.registrationDisabled
|
cfg.ClientAPI.RegistrationDisabled = tc.registrationDisabled
|
||||||
base.Cfg.ClientAPI.GuestsDisabled = tc.guestsDisabled
|
cfg.ClientAPI.GuestsDisabled = tc.guestsDisabled
|
||||||
|
|
||||||
if tc.kind == "" {
|
if tc.kind == "" {
|
||||||
tc.kind = "user"
|
tc.kind = "user"
|
||||||
|
@ -467,19 +487,19 @@ func Test_register(t *testing.T) {
|
||||||
|
|
||||||
req := httptest.NewRequest(http.MethodPost, fmt.Sprintf("/?kind=%s", tc.kind), body)
|
req := httptest.NewRequest(http.MethodPost, fmt.Sprintf("/?kind=%s", tc.kind), body)
|
||||||
|
|
||||||
resp := Register(req, userAPI, &base.Cfg.ClientAPI)
|
resp := Register(req, userAPI, &cfg.ClientAPI)
|
||||||
t.Logf("Resp: %+v", resp)
|
t.Logf("Resp: %+v", resp)
|
||||||
|
|
||||||
// The first request should return a userInteractiveResponse
|
// The first request should return a userInteractiveResponse
|
||||||
switch r := resp.JSON.(type) {
|
switch r := resp.JSON.(type) {
|
||||||
case userInteractiveResponse:
|
case userInteractiveResponse:
|
||||||
// Check that the flows are the ones we configured
|
// Check that the flows are the ones we configured
|
||||||
if !reflect.DeepEqual(r.Flows, base.Cfg.Derived.Registration.Flows) {
|
if !reflect.DeepEqual(r.Flows, cfg.Derived.Registration.Flows) {
|
||||||
t.Fatalf("unexpected registration flows: %+v, want %+v", r.Flows, base.Cfg.Derived.Registration.Flows)
|
t.Fatalf("unexpected registration flows: %+v, want %+v", r.Flows, cfg.Derived.Registration.Flows)
|
||||||
}
|
}
|
||||||
case *jsonerror.MatrixError:
|
case spec.MatrixError:
|
||||||
if !reflect.DeepEqual(tc.wantResponse, resp) {
|
if !reflect.DeepEqual(tc.wantErrorResponse, resp) {
|
||||||
t.Fatalf("(%s), unexpected response: %+v, want: %+v", tc.name, resp, tc.wantResponse)
|
t.Fatalf("(%s), unexpected response: %+v, want: %+v", tc.name, resp, tc.wantErrorResponse)
|
||||||
}
|
}
|
||||||
return
|
return
|
||||||
case registerResponse:
|
case registerResponse:
|
||||||
|
@ -497,6 +517,13 @@ func Test_register(t *testing.T) {
|
||||||
if r.DeviceID == "" {
|
if r.DeviceID == "" {
|
||||||
t.Fatalf("missing deviceID in response")
|
t.Fatalf("missing deviceID in response")
|
||||||
}
|
}
|
||||||
|
// if an expected username is provided, assert that it is a match
|
||||||
|
if tc.wantUsername != "" {
|
||||||
|
wantUserID := strings.ToLower(fmt.Sprintf("@%s:%s", tc.wantUsername, "test"))
|
||||||
|
if wantUserID != r.UserID {
|
||||||
|
t.Fatalf("unexpected userID: %s, want %s", r.UserID, wantUserID)
|
||||||
|
}
|
||||||
|
}
|
||||||
return
|
return
|
||||||
default:
|
default:
|
||||||
t.Logf("Got response: %T", resp.JSON)
|
t.Logf("Got response: %T", resp.JSON)
|
||||||
|
@ -531,41 +558,31 @@ func Test_register(t *testing.T) {
|
||||||
|
|
||||||
req = httptest.NewRequest(http.MethodPost, "/", body)
|
req = httptest.NewRequest(http.MethodPost, "/", body)
|
||||||
|
|
||||||
resp = Register(req, userAPI, &base.Cfg.ClientAPI)
|
resp = Register(req, userAPI, &cfg.ClientAPI)
|
||||||
|
|
||||||
switch resp.JSON.(type) {
|
switch rr := resp.JSON.(type) {
|
||||||
case *jsonerror.MatrixError:
|
case spec.InternalServerError, spec.MatrixError, util.JSONResponse:
|
||||||
if !reflect.DeepEqual(tc.wantResponse, resp) {
|
if !reflect.DeepEqual(tc.wantErrorResponse, resp) {
|
||||||
t.Fatalf("unexpected response: %+v, want: %+v", resp, tc.wantResponse)
|
t.Fatalf("unexpected response: %+v, want: %+v", resp, tc.wantErrorResponse)
|
||||||
}
|
}
|
||||||
return
|
return
|
||||||
case util.JSONResponse:
|
case registerResponse:
|
||||||
if !reflect.DeepEqual(tc.wantResponse, resp) {
|
// validate the response
|
||||||
t.Fatalf("unexpected response: %+v, want: %+v", resp, tc.wantResponse)
|
if tc.wantUsername != "" {
|
||||||
|
// if an expected username is provided, assert that it is a match
|
||||||
|
wantUserID := strings.ToLower(fmt.Sprintf("@%s:%s", tc.wantUsername, "test"))
|
||||||
|
if wantUserID != rr.UserID {
|
||||||
|
t.Fatalf("unexpected userID: %s, want %s", rr.UserID, wantUserID)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return
|
if rr.DeviceID != *reg.DeviceID {
|
||||||
}
|
t.Fatalf("unexpected deviceID: %s, want %s", rr.DeviceID, *reg.DeviceID)
|
||||||
|
}
|
||||||
rr, ok := resp.JSON.(registerResponse)
|
if rr.AccessToken == "" {
|
||||||
if !ok {
|
t.Fatalf("missing accessToken in response")
|
||||||
t.Fatalf("expected a registerresponse, got %T", resp.JSON)
|
}
|
||||||
}
|
default:
|
||||||
|
t.Fatalf("expected one of internalservererror, matrixerror, jsonresponse, registerresponse, got %T", resp.JSON)
|
||||||
// validate the response
|
|
||||||
if tc.forceEmpty {
|
|
||||||
// when not supplying a username, one will be generated. Given this _SHOULD_ be
|
|
||||||
// the second user, set the username accordingly
|
|
||||||
reg.Username = "2"
|
|
||||||
}
|
|
||||||
wantUserID := strings.ToLower(fmt.Sprintf("@%s:%s", reg.Username, "test"))
|
|
||||||
if wantUserID != rr.UserID {
|
|
||||||
t.Fatalf("unexpected userID: %s, want %s", rr.UserID, wantUserID)
|
|
||||||
}
|
|
||||||
if rr.DeviceID != *reg.DeviceID {
|
|
||||||
t.Fatalf("unexpected deviceID: %s, want %s", rr.DeviceID, *reg.DeviceID)
|
|
||||||
}
|
|
||||||
if rr.AccessToken == "" {
|
|
||||||
t.Fatalf("missing accessToken in response")
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@ -574,16 +591,20 @@ func Test_register(t *testing.T) {
|
||||||
|
|
||||||
func TestRegisterUserWithDisplayName(t *testing.T) {
|
func TestRegisterUserWithDisplayName(t *testing.T) {
|
||||||
test.WithAllDatabases(t, func(t *testing.T, dbType test.DBType) {
|
test.WithAllDatabases(t, func(t *testing.T, dbType test.DBType) {
|
||||||
base, baseClose := testrig.CreateBaseDendrite(t, dbType)
|
cfg, processCtx, close := testrig.CreateConfig(t, dbType)
|
||||||
defer baseClose()
|
defer close()
|
||||||
base.Cfg.Global.ServerName = "server"
|
cfg.Global.ServerName = "server"
|
||||||
|
|
||||||
rsAPI := roomserver.NewInternalAPI(base)
|
caches := caching.NewRistrettoCache(128*1024*1024, time.Hour, caching.DisableMetrics)
|
||||||
userAPI := userapi.NewInternalAPI(base, rsAPI, nil)
|
natsInstance := jetstream.NATSInstance{}
|
||||||
|
cm := sqlutil.NewConnectionManager(processCtx, cfg.Global.DatabaseOptions)
|
||||||
|
rsAPI := roomserver.NewInternalAPI(processCtx, cfg, cm, &natsInstance, caches, caching.DisableMetrics)
|
||||||
|
rsAPI.SetFederationAPI(nil, nil)
|
||||||
|
userAPI := userapi.NewInternalAPI(processCtx, cfg, cm, &natsInstance, rsAPI, nil, caching.DisableMetrics, testIsBlacklistedOrBackingOff)
|
||||||
deviceName, deviceID := "deviceName", "deviceID"
|
deviceName, deviceID := "deviceName", "deviceID"
|
||||||
expectedDisplayName := "DisplayName"
|
expectedDisplayName := "DisplayName"
|
||||||
response := completeRegistration(
|
response := completeRegistration(
|
||||||
base.Context(),
|
processCtx.Context(),
|
||||||
userAPI,
|
userAPI,
|
||||||
"user",
|
"user",
|
||||||
"server",
|
"server",
|
||||||
|
@ -601,24 +622,26 @@ func TestRegisterUserWithDisplayName(t *testing.T) {
|
||||||
|
|
||||||
assert.Equal(t, http.StatusOK, response.Code)
|
assert.Equal(t, http.StatusOK, response.Code)
|
||||||
|
|
||||||
req := api.QueryProfileRequest{UserID: "@user:server"}
|
profile, err := userAPI.QueryProfile(processCtx.Context(), "@user:server")
|
||||||
var res api.QueryProfileResponse
|
|
||||||
err := userAPI.QueryProfile(base.Context(), &req, &res)
|
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
assert.Equal(t, expectedDisplayName, res.DisplayName)
|
assert.Equal(t, expectedDisplayName, profile.DisplayName)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestRegisterAdminUsingSharedSecret(t *testing.T) {
|
func TestRegisterAdminUsingSharedSecret(t *testing.T) {
|
||||||
test.WithAllDatabases(t, func(t *testing.T, dbType test.DBType) {
|
test.WithAllDatabases(t, func(t *testing.T, dbType test.DBType) {
|
||||||
base, baseClose := testrig.CreateBaseDendrite(t, dbType)
|
cfg, processCtx, close := testrig.CreateConfig(t, dbType)
|
||||||
defer baseClose()
|
defer close()
|
||||||
base.Cfg.Global.ServerName = "server"
|
natsInstance := jetstream.NATSInstance{}
|
||||||
|
cfg.Global.ServerName = "server"
|
||||||
sharedSecret := "dendritetest"
|
sharedSecret := "dendritetest"
|
||||||
base.Cfg.ClientAPI.RegistrationSharedSecret = sharedSecret
|
cfg.ClientAPI.RegistrationSharedSecret = sharedSecret
|
||||||
|
|
||||||
rsAPI := roomserver.NewInternalAPI(base)
|
cm := sqlutil.NewConnectionManager(processCtx, cfg.Global.DatabaseOptions)
|
||||||
userAPI := userapi.NewInternalAPI(base, rsAPI, nil)
|
caches := caching.NewRistrettoCache(128*1024*1024, time.Hour, caching.DisableMetrics)
|
||||||
|
rsAPI := roomserver.NewInternalAPI(processCtx, cfg, cm, &natsInstance, caches, caching.DisableMetrics)
|
||||||
|
rsAPI.SetFederationAPI(nil, nil)
|
||||||
|
userAPI := userapi.NewInternalAPI(processCtx, cfg, cm, &natsInstance, rsAPI, nil, caching.DisableMetrics, testIsBlacklistedOrBackingOff)
|
||||||
|
|
||||||
expectedDisplayName := "rabbit"
|
expectedDisplayName := "rabbit"
|
||||||
jsonStr := []byte(`{"admin":true,"mac":"24dca3bba410e43fe64b9b5c28306693bf3baa9f","nonce":"759f047f312b99ff428b21d581256f8592b8976e58bc1b543972dc6147e529a79657605b52d7becd160ff5137f3de11975684319187e06901955f79e5a6c5a79","password":"wonderland","username":"alice","displayname":"rabbit"}`)
|
jsonStr := []byte(`{"admin":true,"mac":"24dca3bba410e43fe64b9b5c28306693bf3baa9f","nonce":"759f047f312b99ff428b21d581256f8592b8976e58bc1b543972dc6147e529a79657605b52d7becd160ff5137f3de11975684319187e06901955f79e5a6c5a79","password":"wonderland","username":"alice","displayname":"rabbit"}`)
|
||||||
|
@ -642,17 +665,15 @@ func TestRegisterAdminUsingSharedSecret(t *testing.T) {
|
||||||
ssrr := httptest.NewRequest(http.MethodPost, "/", body)
|
ssrr := httptest.NewRequest(http.MethodPost, "/", body)
|
||||||
|
|
||||||
response := handleSharedSecretRegistration(
|
response := handleSharedSecretRegistration(
|
||||||
&base.Cfg.ClientAPI,
|
&cfg.ClientAPI,
|
||||||
userAPI,
|
userAPI,
|
||||||
r,
|
r,
|
||||||
ssrr,
|
ssrr,
|
||||||
)
|
)
|
||||||
assert.Equal(t, http.StatusOK, response.Code)
|
assert.Equal(t, http.StatusOK, response.Code)
|
||||||
|
|
||||||
profilReq := api.QueryProfileRequest{UserID: "@alice:server"}
|
profile, err := userAPI.QueryProfile(processCtx.Context(), "@alice:server")
|
||||||
var profileRes api.QueryProfileResponse
|
|
||||||
err = userAPI.QueryProfile(base.Context(), &profilReq, &profileRes)
|
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
assert.Equal(t, expectedDisplayName, profileRes.DisplayName)
|
assert.Equal(t, expectedDisplayName, profile.DisplayName)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
93
clientapi/routing/report_event.go
Normal file
93
clientapi/routing/report_event.go
Normal file
|
@ -0,0 +1,93 @@
|
||||||
|
// Copyright 2023 The Matrix.org Foundation C.I.C.
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
package routing
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/matrix-org/dendrite/clientapi/httputil"
|
||||||
|
"github.com/matrix-org/dendrite/roomserver/api"
|
||||||
|
userAPI "github.com/matrix-org/dendrite/userapi/api"
|
||||||
|
"github.com/matrix-org/gomatrixserverlib/spec"
|
||||||
|
"github.com/matrix-org/util"
|
||||||
|
)
|
||||||
|
|
||||||
|
type reportEventRequest struct {
|
||||||
|
Reason string `json:"reason"`
|
||||||
|
Score int64 `json:"score"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func ReportEvent(
|
||||||
|
req *http.Request,
|
||||||
|
device *userAPI.Device,
|
||||||
|
roomID, eventID string,
|
||||||
|
rsAPI api.ClientRoomserverAPI,
|
||||||
|
) util.JSONResponse {
|
||||||
|
defer req.Body.Close() // nolint: errcheck
|
||||||
|
|
||||||
|
deviceUserID, err := spec.NewUserID(device.UserID, true)
|
||||||
|
if err != nil {
|
||||||
|
return util.JSONResponse{
|
||||||
|
Code: http.StatusForbidden,
|
||||||
|
JSON: spec.NotFound("You don't have permission to report this event, bad userID"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// The requesting user must be a member of the room
|
||||||
|
errRes := checkMemberInRoom(req.Context(), rsAPI, *deviceUserID, roomID)
|
||||||
|
if errRes != nil {
|
||||||
|
return util.JSONResponse{
|
||||||
|
Code: http.StatusNotFound, // Spec demands this...
|
||||||
|
JSON: spec.NotFound("The event was not found or you are not joined to the room."),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse the request
|
||||||
|
report := reportEventRequest{}
|
||||||
|
if resErr := httputil.UnmarshalJSONRequest(req, &report); resErr != nil {
|
||||||
|
return *resErr
|
||||||
|
}
|
||||||
|
|
||||||
|
queryRes := &api.QueryEventsByIDResponse{}
|
||||||
|
if err = rsAPI.QueryEventsByID(req.Context(), &api.QueryEventsByIDRequest{
|
||||||
|
RoomID: roomID,
|
||||||
|
EventIDs: []string{eventID},
|
||||||
|
}, queryRes); err != nil {
|
||||||
|
return util.JSONResponse{
|
||||||
|
Code: http.StatusInternalServerError,
|
||||||
|
JSON: spec.InternalServerError{Err: err.Error()},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// No event was found or it was already redacted
|
||||||
|
if len(queryRes.Events) == 0 || queryRes.Events[0].Redacted() {
|
||||||
|
return util.JSONResponse{
|
||||||
|
Code: http.StatusNotFound,
|
||||||
|
JSON: spec.NotFound("The event was not found or you are not joined to the room."),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = rsAPI.InsertReportedEvent(req.Context(), roomID, eventID, device.UserID, report.Reason, report.Score)
|
||||||
|
if err != nil {
|
||||||
|
return util.JSONResponse{
|
||||||
|
Code: http.StatusInternalServerError,
|
||||||
|
JSON: spec.InternalServerError{Err: err.Error()},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return util.JSONResponse{
|
||||||
|
Code: http.StatusOK,
|
||||||
|
JSON: struct{}{},
|
||||||
|
}
|
||||||
|
}
|
180
clientapi/routing/room_hierarchy.go
Normal file
180
clientapi/routing/room_hierarchy.go
Normal file
|
@ -0,0 +1,180 @@
|
||||||
|
// Copyright 2023 The Matrix.org Foundation C.I.C.
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
package routing
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
"strconv"
|
||||||
|
"sync"
|
||||||
|
|
||||||
|
"github.com/google/uuid"
|
||||||
|
roomserverAPI "github.com/matrix-org/dendrite/roomserver/api"
|
||||||
|
"github.com/matrix-org/dendrite/roomserver/types"
|
||||||
|
userapi "github.com/matrix-org/dendrite/userapi/api"
|
||||||
|
"github.com/matrix-org/gomatrixserverlib/fclient"
|
||||||
|
"github.com/matrix-org/gomatrixserverlib/spec"
|
||||||
|
"github.com/matrix-org/util"
|
||||||
|
log "github.com/sirupsen/logrus"
|
||||||
|
)
|
||||||
|
|
||||||
|
// For storing pagination information for room hierarchies
|
||||||
|
type RoomHierarchyPaginationCache struct {
|
||||||
|
cache map[string]roomserverAPI.RoomHierarchyWalker
|
||||||
|
mu sync.Mutex
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create a new, empty, pagination cache.
|
||||||
|
func NewRoomHierarchyPaginationCache() RoomHierarchyPaginationCache {
|
||||||
|
return RoomHierarchyPaginationCache{
|
||||||
|
cache: map[string]roomserverAPI.RoomHierarchyWalker{},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get a cached page, or nil if there is no associated page in the cache.
|
||||||
|
func (c *RoomHierarchyPaginationCache) Get(token string) *roomserverAPI.RoomHierarchyWalker {
|
||||||
|
c.mu.Lock()
|
||||||
|
defer c.mu.Unlock()
|
||||||
|
line, ok := c.cache[token]
|
||||||
|
if ok {
|
||||||
|
return &line
|
||||||
|
} else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add a cache line to the pagination cache.
|
||||||
|
func (c *RoomHierarchyPaginationCache) AddLine(line roomserverAPI.RoomHierarchyWalker) string {
|
||||||
|
c.mu.Lock()
|
||||||
|
defer c.mu.Unlock()
|
||||||
|
token := uuid.NewString()
|
||||||
|
c.cache[token] = line
|
||||||
|
return token
|
||||||
|
}
|
||||||
|
|
||||||
|
// Query the hierarchy of a room/space
|
||||||
|
//
|
||||||
|
// Implements /_matrix/client/v1/rooms/{roomID}/hierarchy
|
||||||
|
func QueryRoomHierarchy(req *http.Request, device *userapi.Device, roomIDStr string, rsAPI roomserverAPI.ClientRoomserverAPI, paginationCache *RoomHierarchyPaginationCache) util.JSONResponse {
|
||||||
|
parsedRoomID, err := spec.NewRoomID(roomIDStr)
|
||||||
|
if err != nil {
|
||||||
|
return util.JSONResponse{
|
||||||
|
Code: http.StatusNotFound,
|
||||||
|
JSON: spec.InvalidParam("room is unknown/forbidden"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
roomID := *parsedRoomID
|
||||||
|
|
||||||
|
suggestedOnly := false // Defaults to false (spec-defined)
|
||||||
|
switch req.URL.Query().Get("suggested_only") {
|
||||||
|
case "true":
|
||||||
|
suggestedOnly = true
|
||||||
|
case "false":
|
||||||
|
case "": // Empty string is returned when query param is not set
|
||||||
|
default:
|
||||||
|
return util.JSONResponse{
|
||||||
|
Code: http.StatusBadRequest,
|
||||||
|
JSON: spec.InvalidParam("query parameter 'suggested_only', if set, must be 'true' or 'false'"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
limit := 1000 // Default to 1000
|
||||||
|
limitStr := req.URL.Query().Get("limit")
|
||||||
|
if limitStr != "" {
|
||||||
|
var maybeLimit int
|
||||||
|
maybeLimit, err = strconv.Atoi(limitStr)
|
||||||
|
if err != nil || maybeLimit < 0 {
|
||||||
|
return util.JSONResponse{
|
||||||
|
Code: http.StatusBadRequest,
|
||||||
|
JSON: spec.InvalidParam("query parameter 'limit', if set, must be a positive integer"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
limit = maybeLimit
|
||||||
|
if limit > 1000 {
|
||||||
|
limit = 1000 // Maximum limit of 1000
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
maxDepth := -1 // '-1' representing no maximum depth
|
||||||
|
maxDepthStr := req.URL.Query().Get("max_depth")
|
||||||
|
if maxDepthStr != "" {
|
||||||
|
var maybeMaxDepth int
|
||||||
|
maybeMaxDepth, err = strconv.Atoi(maxDepthStr)
|
||||||
|
if err != nil || maybeMaxDepth < 0 {
|
||||||
|
return util.JSONResponse{
|
||||||
|
Code: http.StatusBadRequest,
|
||||||
|
JSON: spec.InvalidParam("query parameter 'max_depth', if set, must be a positive integer"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
maxDepth = maybeMaxDepth
|
||||||
|
}
|
||||||
|
|
||||||
|
from := req.URL.Query().Get("from")
|
||||||
|
|
||||||
|
var walker roomserverAPI.RoomHierarchyWalker
|
||||||
|
if from == "" { // No pagination token provided, so start new hierarchy walker
|
||||||
|
walker = roomserverAPI.NewRoomHierarchyWalker(types.NewDeviceNotServerName(*device), roomID, suggestedOnly, maxDepth)
|
||||||
|
} else { // Attempt to resume cached walker
|
||||||
|
cachedWalker := paginationCache.Get(from)
|
||||||
|
|
||||||
|
if cachedWalker == nil || cachedWalker.SuggestedOnly != suggestedOnly || cachedWalker.MaxDepth != maxDepth {
|
||||||
|
return util.JSONResponse{
|
||||||
|
Code: http.StatusBadRequest,
|
||||||
|
JSON: spec.InvalidParam("pagination not found for provided token ('from') with given 'max_depth', 'suggested_only' and room ID"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
walker = *cachedWalker
|
||||||
|
}
|
||||||
|
|
||||||
|
discoveredRooms, _, nextWalker, err := rsAPI.QueryNextRoomHierarchyPage(req.Context(), walker, limit)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
switch err.(type) {
|
||||||
|
case roomserverAPI.ErrRoomUnknownOrNotAllowed:
|
||||||
|
util.GetLogger(req.Context()).WithError(err).Debugln("room unknown/forbidden when handling CS room hierarchy request")
|
||||||
|
return util.JSONResponse{
|
||||||
|
Code: http.StatusForbidden,
|
||||||
|
JSON: spec.Forbidden("room is unknown/forbidden"),
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
log.WithError(err).Errorf("failed to fetch next page of room hierarchy (CS API)")
|
||||||
|
return util.JSONResponse{
|
||||||
|
Code: http.StatusInternalServerError,
|
||||||
|
JSON: spec.Unknown("internal server error"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
nextBatch := ""
|
||||||
|
// nextWalker will be nil if there's no more rooms left to walk
|
||||||
|
if nextWalker != nil {
|
||||||
|
nextBatch = paginationCache.AddLine(*nextWalker)
|
||||||
|
}
|
||||||
|
|
||||||
|
return util.JSONResponse{
|
||||||
|
Code: http.StatusOK,
|
||||||
|
JSON: RoomHierarchyClientResponse{
|
||||||
|
Rooms: discoveredRooms,
|
||||||
|
NextBatch: nextBatch,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// Success response for /_matrix/client/v1/rooms/{roomID}/hierarchy
|
||||||
|
type RoomHierarchyClientResponse struct {
|
||||||
|
Rooms []fclient.RoomHierarchyRoom `json:"rooms"`
|
||||||
|
NextBatch string `json:"next_batch,omitempty"`
|
||||||
|
}
|
|
@ -19,10 +19,10 @@ import (
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
"github.com/matrix-org/dendrite/clientapi/httputil"
|
"github.com/matrix-org/dendrite/clientapi/httputil"
|
||||||
"github.com/matrix-org/dendrite/clientapi/jsonerror"
|
|
||||||
"github.com/matrix-org/dendrite/clientapi/producers"
|
"github.com/matrix-org/dendrite/clientapi/producers"
|
||||||
"github.com/matrix-org/dendrite/userapi/api"
|
"github.com/matrix-org/dendrite/userapi/api"
|
||||||
"github.com/matrix-org/gomatrix"
|
"github.com/matrix-org/gomatrix"
|
||||||
|
"github.com/matrix-org/gomatrixserverlib/spec"
|
||||||
"github.com/matrix-org/util"
|
"github.com/matrix-org/util"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -39,14 +39,17 @@ func GetTags(
|
||||||
if device.UserID != userID {
|
if device.UserID != userID {
|
||||||
return util.JSONResponse{
|
return util.JSONResponse{
|
||||||
Code: http.StatusForbidden,
|
Code: http.StatusForbidden,
|
||||||
JSON: jsonerror.Forbidden("Cannot retrieve another user's tags"),
|
JSON: spec.Forbidden("Cannot retrieve another user's tags"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
tagContent, err := obtainSavedTags(req, userID, roomID, userAPI)
|
tagContent, err := obtainSavedTags(req, userID, roomID, userAPI)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
util.GetLogger(req.Context()).WithError(err).Error("obtainSavedTags failed")
|
util.GetLogger(req.Context()).WithError(err).Error("obtainSavedTags failed")
|
||||||
return jsonerror.InternalServerError()
|
return util.JSONResponse{
|
||||||
|
Code: http.StatusInternalServerError,
|
||||||
|
JSON: spec.InternalServerError{},
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return util.JSONResponse{
|
return util.JSONResponse{
|
||||||
|
@ -71,7 +74,7 @@ func PutTag(
|
||||||
if device.UserID != userID {
|
if device.UserID != userID {
|
||||||
return util.JSONResponse{
|
return util.JSONResponse{
|
||||||
Code: http.StatusForbidden,
|
Code: http.StatusForbidden,
|
||||||
JSON: jsonerror.Forbidden("Cannot modify another user's tags"),
|
JSON: spec.Forbidden("Cannot modify another user's tags"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -83,7 +86,10 @@ func PutTag(
|
||||||
tagContent, err := obtainSavedTags(req, userID, roomID, userAPI)
|
tagContent, err := obtainSavedTags(req, userID, roomID, userAPI)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
util.GetLogger(req.Context()).WithError(err).Error("obtainSavedTags failed")
|
util.GetLogger(req.Context()).WithError(err).Error("obtainSavedTags failed")
|
||||||
return jsonerror.InternalServerError()
|
return util.JSONResponse{
|
||||||
|
Code: http.StatusInternalServerError,
|
||||||
|
JSON: spec.InternalServerError{},
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if tagContent.Tags == nil {
|
if tagContent.Tags == nil {
|
||||||
|
@ -93,7 +99,10 @@ func PutTag(
|
||||||
|
|
||||||
if err = saveTagData(req, userID, roomID, userAPI, tagContent); err != nil {
|
if err = saveTagData(req, userID, roomID, userAPI, tagContent); err != nil {
|
||||||
util.GetLogger(req.Context()).WithError(err).Error("saveTagData failed")
|
util.GetLogger(req.Context()).WithError(err).Error("saveTagData failed")
|
||||||
return jsonerror.InternalServerError()
|
return util.JSONResponse{
|
||||||
|
Code: http.StatusInternalServerError,
|
||||||
|
JSON: spec.InternalServerError{},
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return util.JSONResponse{
|
return util.JSONResponse{
|
||||||
|
@ -118,14 +127,17 @@ func DeleteTag(
|
||||||
if device.UserID != userID {
|
if device.UserID != userID {
|
||||||
return util.JSONResponse{
|
return util.JSONResponse{
|
||||||
Code: http.StatusForbidden,
|
Code: http.StatusForbidden,
|
||||||
JSON: jsonerror.Forbidden("Cannot modify another user's tags"),
|
JSON: spec.Forbidden("Cannot modify another user's tags"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
tagContent, err := obtainSavedTags(req, userID, roomID, userAPI)
|
tagContent, err := obtainSavedTags(req, userID, roomID, userAPI)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
util.GetLogger(req.Context()).WithError(err).Error("obtainSavedTags failed")
|
util.GetLogger(req.Context()).WithError(err).Error("obtainSavedTags failed")
|
||||||
return jsonerror.InternalServerError()
|
return util.JSONResponse{
|
||||||
|
Code: http.StatusInternalServerError,
|
||||||
|
JSON: spec.InternalServerError{},
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check whether the tag to be deleted exists
|
// Check whether the tag to be deleted exists
|
||||||
|
@ -141,7 +153,10 @@ func DeleteTag(
|
||||||
|
|
||||||
if err = saveTagData(req, userID, roomID, userAPI, tagContent); err != nil {
|
if err = saveTagData(req, userID, roomID, userAPI, tagContent); err != nil {
|
||||||
util.GetLogger(req.Context()).WithError(err).Error("saveTagData failed")
|
util.GetLogger(req.Context()).WithError(err).Error("saveTagData failed")
|
||||||
return jsonerror.InternalServerError()
|
return util.JSONResponse{
|
||||||
|
Code: http.StatusInternalServerError,
|
||||||
|
JSON: spec.InternalServerError{},
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return util.JSONResponse{
|
return util.JSONResponse{
|
||||||
|
|
|
@ -18,22 +18,23 @@ import (
|
||||||
"context"
|
"context"
|
||||||
"net/http"
|
"net/http"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
|
||||||
|
|
||||||
"github.com/gorilla/mux"
|
"github.com/gorilla/mux"
|
||||||
"github.com/matrix-org/dendrite/setup/base"
|
"github.com/matrix-org/gomatrixserverlib/fclient"
|
||||||
userapi "github.com/matrix-org/dendrite/userapi/api"
|
"github.com/matrix-org/gomatrixserverlib/spec"
|
||||||
"github.com/matrix-org/gomatrixserverlib"
|
|
||||||
"github.com/matrix-org/util"
|
"github.com/matrix-org/util"
|
||||||
"github.com/nats-io/nats.go"
|
"github.com/nats-io/nats.go"
|
||||||
"github.com/prometheus/client_golang/prometheus"
|
"github.com/prometheus/client_golang/prometheus"
|
||||||
"github.com/sirupsen/logrus"
|
"github.com/sirupsen/logrus"
|
||||||
|
"golang.org/x/sync/singleflight"
|
||||||
|
|
||||||
|
"github.com/matrix-org/dendrite/setup/base"
|
||||||
|
userapi "github.com/matrix-org/dendrite/userapi/api"
|
||||||
|
|
||||||
appserviceAPI "github.com/matrix-org/dendrite/appservice/api"
|
appserviceAPI "github.com/matrix-org/dendrite/appservice/api"
|
||||||
"github.com/matrix-org/dendrite/clientapi/api"
|
"github.com/matrix-org/dendrite/clientapi/api"
|
||||||
"github.com/matrix-org/dendrite/clientapi/auth"
|
"github.com/matrix-org/dendrite/clientapi/auth"
|
||||||
clientutil "github.com/matrix-org/dendrite/clientapi/httputil"
|
clientutil "github.com/matrix-org/dendrite/clientapi/httputil"
|
||||||
"github.com/matrix-org/dendrite/clientapi/jsonerror"
|
|
||||||
"github.com/matrix-org/dendrite/clientapi/producers"
|
"github.com/matrix-org/dendrite/clientapi/producers"
|
||||||
federationAPI "github.com/matrix-org/dendrite/federationapi/api"
|
federationAPI "github.com/matrix-org/dendrite/federationapi/api"
|
||||||
"github.com/matrix-org/dendrite/internal/httputil"
|
"github.com/matrix-org/dendrite/internal/httputil"
|
||||||
|
@ -43,6 +44,19 @@ import (
|
||||||
"github.com/matrix-org/dendrite/setup/jetstream"
|
"github.com/matrix-org/dendrite/setup/jetstream"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
type WellKnownClientHomeserver struct {
|
||||||
|
BaseUrl string `json:"base_url"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type WellKnownSlidingSyncProxy struct {
|
||||||
|
Url string `json:"url"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type WellKnownClientResponse struct {
|
||||||
|
Homeserver WellKnownClientHomeserver `json:"m.homeserver"`
|
||||||
|
SlidingSyncProxy *WellKnownSlidingSyncProxy `json:"org.matrix.msc3575.proxy,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
// Setup registers HTTP handlers with the given ServeMux. It also supplies the given http.Client
|
// Setup registers HTTP handlers with the given ServeMux. It also supplies the given http.Client
|
||||||
// to clients which need to make outbound HTTP requests.
|
// to clients which need to make outbound HTTP requests.
|
||||||
//
|
//
|
||||||
|
@ -50,25 +64,27 @@ import (
|
||||||
// applied:
|
// applied:
|
||||||
// nolint: gocyclo
|
// nolint: gocyclo
|
||||||
func Setup(
|
func Setup(
|
||||||
base *base.BaseDendrite,
|
routers httputil.Routers,
|
||||||
cfg *config.ClientAPI,
|
dendriteCfg *config.Dendrite,
|
||||||
rsAPI roomserverAPI.ClientRoomserverAPI,
|
rsAPI roomserverAPI.ClientRoomserverAPI,
|
||||||
asAPI appserviceAPI.AppServiceInternalAPI,
|
asAPI appserviceAPI.AppServiceInternalAPI,
|
||||||
userAPI userapi.ClientUserAPI,
|
userAPI userapi.ClientUserAPI,
|
||||||
userDirectoryProvider userapi.QuerySearchProfilesAPI,
|
userDirectoryProvider userapi.QuerySearchProfilesAPI,
|
||||||
federation *gomatrixserverlib.FederationClient,
|
federation fclient.FederationClient,
|
||||||
syncProducer *producers.SyncAPIProducer,
|
syncProducer *producers.SyncAPIProducer,
|
||||||
transactionsCache *transactions.Cache,
|
transactionsCache *transactions.Cache,
|
||||||
federationSender federationAPI.ClientFederationAPI,
|
federationSender federationAPI.ClientFederationAPI,
|
||||||
extRoomsProvider api.ExtraPublicRoomsProvider,
|
extRoomsProvider api.ExtraPublicRoomsProvider,
|
||||||
mscCfg *config.MSCs, natsClient *nats.Conn,
|
natsClient *nats.Conn, enableMetrics bool,
|
||||||
) {
|
) {
|
||||||
publicAPIMux := base.PublicClientAPIMux
|
cfg := &dendriteCfg.ClientAPI
|
||||||
wkMux := base.PublicWellKnownAPIMux
|
mscCfg := &dendriteCfg.MSCs
|
||||||
synapseAdminRouter := base.SynapseAdminMux
|
publicAPIMux := routers.Client
|
||||||
dendriteAdminRouter := base.DendriteAdminMux
|
wkMux := routers.WellKnown
|
||||||
|
synapseAdminRouter := routers.SynapseAdmin
|
||||||
|
dendriteAdminRouter := routers.DendriteAdmin
|
||||||
|
|
||||||
if base.EnableMetrics {
|
if enableMetrics {
|
||||||
prometheus.MustRegister(amtRegUsers, sendEventDuration)
|
prometheus.MustRegister(amtRegUsers, sendEventDuration)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -83,22 +99,32 @@ func Setup(
|
||||||
unstableFeatures["org.matrix."+msc] = true
|
unstableFeatures["org.matrix."+msc] = true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// singleflight protects /join endpoints from being invoked
|
||||||
|
// multiple times from the same user and room, otherwise
|
||||||
|
// a state reset can occur. This also avoids unneeded
|
||||||
|
// state calculations.
|
||||||
|
// TODO: actually fix this in the roomserver, as there are
|
||||||
|
// possibly other ways that can result in a stat reset.
|
||||||
|
sf := singleflight.Group{}
|
||||||
|
|
||||||
if cfg.Matrix.WellKnownClientName != "" {
|
if cfg.Matrix.WellKnownClientName != "" {
|
||||||
logrus.Infof("Setting m.homeserver base_url as %s at /.well-known/matrix/client", cfg.Matrix.WellKnownClientName)
|
logrus.Infof("Setting m.homeserver base_url as %s at /.well-known/matrix/client", cfg.Matrix.WellKnownClientName)
|
||||||
|
if cfg.Matrix.WellKnownSlidingSyncProxy != "" {
|
||||||
|
logrus.Infof("Setting org.matrix.msc3575.proxy url as %s at /.well-known/matrix/client", cfg.Matrix.WellKnownSlidingSyncProxy)
|
||||||
|
}
|
||||||
wkMux.Handle("/client", httputil.MakeExternalAPI("wellknown", func(r *http.Request) util.JSONResponse {
|
wkMux.Handle("/client", httputil.MakeExternalAPI("wellknown", func(r *http.Request) util.JSONResponse {
|
||||||
|
response := WellKnownClientResponse{
|
||||||
|
Homeserver: WellKnownClientHomeserver{cfg.Matrix.WellKnownClientName},
|
||||||
|
}
|
||||||
|
if cfg.Matrix.WellKnownSlidingSyncProxy != "" {
|
||||||
|
response.SlidingSyncProxy = &WellKnownSlidingSyncProxy{
|
||||||
|
Url: cfg.Matrix.WellKnownSlidingSyncProxy,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return util.JSONResponse{
|
return util.JSONResponse{
|
||||||
Code: http.StatusOK,
|
Code: http.StatusOK,
|
||||||
JSON: struct {
|
JSON: response,
|
||||||
HomeserverName struct {
|
|
||||||
BaseUrl string `json:"base_url"`
|
|
||||||
} `json:"m.homeserver"`
|
|
||||||
}{
|
|
||||||
HomeserverName: struct {
|
|
||||||
BaseUrl string `json:"base_url"`
|
|
||||||
}{
|
|
||||||
BaseUrl: cfg.Matrix.WellKnownClientName,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
})).Methods(http.MethodGet, http.MethodOptions)
|
})).Methods(http.MethodGet, http.MethodOptions)
|
||||||
}
|
}
|
||||||
|
@ -146,27 +172,57 @@ func Setup(
|
||||||
}
|
}
|
||||||
return util.JSONResponse{
|
return util.JSONResponse{
|
||||||
Code: http.StatusMethodNotAllowed,
|
Code: http.StatusMethodNotAllowed,
|
||||||
JSON: jsonerror.NotFound("unknown method"),
|
JSON: spec.NotFound("unknown method"),
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
).Methods(http.MethodGet, http.MethodPost, http.MethodOptions)
|
).Methods(http.MethodGet, http.MethodPost, http.MethodOptions)
|
||||||
}
|
}
|
||||||
|
dendriteAdminRouter.Handle("/admin/registrationTokens/new",
|
||||||
|
httputil.MakeAdminAPI("admin_registration_tokens_new", userAPI, func(req *http.Request, device *userapi.Device) util.JSONResponse {
|
||||||
|
return AdminCreateNewRegistrationToken(req, cfg, userAPI)
|
||||||
|
}),
|
||||||
|
).Methods(http.MethodPost, http.MethodOptions)
|
||||||
|
|
||||||
|
dendriteAdminRouter.Handle("/admin/registrationTokens",
|
||||||
|
httputil.MakeAdminAPI("admin_list_registration_tokens", userAPI, func(req *http.Request, device *userapi.Device) util.JSONResponse {
|
||||||
|
return AdminListRegistrationTokens(req, cfg, userAPI)
|
||||||
|
}),
|
||||||
|
).Methods(http.MethodGet, http.MethodOptions)
|
||||||
|
|
||||||
|
dendriteAdminRouter.Handle("/admin/registrationTokens/{token}",
|
||||||
|
httputil.MakeAdminAPI("admin_get_registration_token", userAPI, func(req *http.Request, device *userapi.Device) util.JSONResponse {
|
||||||
|
switch req.Method {
|
||||||
|
case http.MethodGet:
|
||||||
|
return AdminGetRegistrationToken(req, cfg, userAPI)
|
||||||
|
case http.MethodPut:
|
||||||
|
return AdminUpdateRegistrationToken(req, cfg, userAPI)
|
||||||
|
case http.MethodDelete:
|
||||||
|
return AdminDeleteRegistrationToken(req, cfg, userAPI)
|
||||||
|
default:
|
||||||
|
return util.MatrixErrorResponse(
|
||||||
|
404,
|
||||||
|
string(spec.ErrorNotFound),
|
||||||
|
"unknown method",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
).Methods(http.MethodGet, http.MethodPut, http.MethodDelete, http.MethodOptions)
|
||||||
|
|
||||||
dendriteAdminRouter.Handle("/admin/evacuateRoom/{roomID}",
|
dendriteAdminRouter.Handle("/admin/evacuateRoom/{roomID}",
|
||||||
httputil.MakeAdminAPI("admin_evacuate_room", userAPI, func(req *http.Request, device *userapi.Device) util.JSONResponse {
|
httputil.MakeAdminAPI("admin_evacuate_room", userAPI, func(req *http.Request, device *userapi.Device) util.JSONResponse {
|
||||||
return AdminEvacuateRoom(req, cfg, device, rsAPI)
|
return AdminEvacuateRoom(req, rsAPI)
|
||||||
}),
|
}),
|
||||||
).Methods(http.MethodGet, http.MethodOptions)
|
).Methods(http.MethodPost, http.MethodOptions)
|
||||||
|
|
||||||
dendriteAdminRouter.Handle("/admin/evacuateUser/{userID}",
|
dendriteAdminRouter.Handle("/admin/evacuateUser/{userID}",
|
||||||
httputil.MakeAdminAPI("admin_evacuate_user", userAPI, func(req *http.Request, device *userapi.Device) util.JSONResponse {
|
httputil.MakeAdminAPI("admin_evacuate_user", userAPI, func(req *http.Request, device *userapi.Device) util.JSONResponse {
|
||||||
return AdminEvacuateUser(req, cfg, device, rsAPI)
|
return AdminEvacuateUser(req, rsAPI)
|
||||||
}),
|
}),
|
||||||
).Methods(http.MethodGet, http.MethodOptions)
|
).Methods(http.MethodPost, http.MethodOptions)
|
||||||
|
|
||||||
dendriteAdminRouter.Handle("/admin/purgeRoom/{roomID}",
|
dendriteAdminRouter.Handle("/admin/purgeRoom/{roomID}",
|
||||||
httputil.MakeAdminAPI("admin_purge_room", userAPI, func(req *http.Request, device *userapi.Device) util.JSONResponse {
|
httputil.MakeAdminAPI("admin_purge_room", userAPI, func(req *http.Request, device *userapi.Device) util.JSONResponse {
|
||||||
return AdminPurgeRoom(req, cfg, device, rsAPI)
|
return AdminPurgeRoom(req, rsAPI)
|
||||||
}),
|
}),
|
||||||
).Methods(http.MethodPost, http.MethodOptions)
|
).Methods(http.MethodPost, http.MethodOptions)
|
||||||
|
|
||||||
|
@ -178,7 +234,7 @@ func Setup(
|
||||||
|
|
||||||
dendriteAdminRouter.Handle("/admin/downloadState/{serverName}/{roomID}",
|
dendriteAdminRouter.Handle("/admin/downloadState/{serverName}/{roomID}",
|
||||||
httputil.MakeAdminAPI("admin_download_state", userAPI, func(req *http.Request, device *userapi.Device) util.JSONResponse {
|
httputil.MakeAdminAPI("admin_download_state", userAPI, func(req *http.Request, device *userapi.Device) util.JSONResponse {
|
||||||
return AdminDownloadState(req, cfg, device, rsAPI)
|
return AdminDownloadState(req, device, rsAPI)
|
||||||
}),
|
}),
|
||||||
).Methods(http.MethodGet, http.MethodOptions)
|
).Methods(http.MethodGet, http.MethodOptions)
|
||||||
|
|
||||||
|
@ -197,18 +253,13 @@ func Setup(
|
||||||
// server notifications
|
// server notifications
|
||||||
if cfg.Matrix.ServerNotices.Enabled {
|
if cfg.Matrix.ServerNotices.Enabled {
|
||||||
logrus.Info("Enabling server notices at /_synapse/admin/v1/send_server_notice")
|
logrus.Info("Enabling server notices at /_synapse/admin/v1/send_server_notice")
|
||||||
var serverNotificationSender *userapi.Device
|
serverNotificationSender, err := getSenderDevice(context.Background(), rsAPI, userAPI, cfg)
|
||||||
var err error
|
if err != nil {
|
||||||
notificationSenderOnce := &sync.Once{}
|
logrus.WithError(err).Fatal("unable to get account for sending server notices")
|
||||||
|
}
|
||||||
|
|
||||||
synapseAdminRouter.Handle("/admin/v1/send_server_notice/{txnID}",
|
synapseAdminRouter.Handle("/admin/v1/send_server_notice/{txnID}",
|
||||||
httputil.MakeAuthAPI("send_server_notice", userAPI, func(req *http.Request, device *userapi.Device) util.JSONResponse {
|
httputil.MakeAuthAPI("send_server_notice", userAPI, func(req *http.Request, device *userapi.Device) util.JSONResponse {
|
||||||
notificationSenderOnce.Do(func() {
|
|
||||||
serverNotificationSender, err = getSenderDevice(context.Background(), rsAPI, userAPI, cfg)
|
|
||||||
if err != nil {
|
|
||||||
logrus.WithError(err).Fatal("unable to get account for sending sending server notices")
|
|
||||||
}
|
|
||||||
})
|
|
||||||
// not specced, but ensure we're rate limiting requests to this endpoint
|
// not specced, but ensure we're rate limiting requests to this endpoint
|
||||||
if r := rateLimits.Limit(req, device); r != nil {
|
if r := rateLimits.Limit(req, device); r != nil {
|
||||||
return *r
|
return *r
|
||||||
|
@ -230,12 +281,6 @@ func Setup(
|
||||||
|
|
||||||
synapseAdminRouter.Handle("/admin/v1/send_server_notice",
|
synapseAdminRouter.Handle("/admin/v1/send_server_notice",
|
||||||
httputil.MakeAuthAPI("send_server_notice", userAPI, func(req *http.Request, device *userapi.Device) util.JSONResponse {
|
httputil.MakeAuthAPI("send_server_notice", userAPI, func(req *http.Request, device *userapi.Device) util.JSONResponse {
|
||||||
notificationSenderOnce.Do(func() {
|
|
||||||
serverNotificationSender, err = getSenderDevice(context.Background(), rsAPI, userAPI, cfg)
|
|
||||||
if err != nil {
|
|
||||||
logrus.WithError(err).Fatal("unable to get account for sending sending server notices")
|
|
||||||
}
|
|
||||||
})
|
|
||||||
// not specced, but ensure we're rate limiting requests to this endpoint
|
// not specced, but ensure we're rate limiting requests to this endpoint
|
||||||
if r := rateLimits.Limit(req, device); r != nil {
|
if r := rateLimits.Limit(req, device); r != nil {
|
||||||
return *r
|
return *r
|
||||||
|
@ -258,6 +303,8 @@ func Setup(
|
||||||
// Note that 'apiversion' is chosen because it must not collide with a variable used in any of the routing!
|
// Note that 'apiversion' is chosen because it must not collide with a variable used in any of the routing!
|
||||||
v3mux := publicAPIMux.PathPrefix("/{apiversion:(?:r0|v3)}/").Subrouter()
|
v3mux := publicAPIMux.PathPrefix("/{apiversion:(?:r0|v3)}/").Subrouter()
|
||||||
|
|
||||||
|
v1mux := publicAPIMux.PathPrefix("/v1/").Subrouter()
|
||||||
|
|
||||||
unstableMux := publicAPIMux.PathPrefix("/unstable").Subrouter()
|
unstableMux := publicAPIMux.PathPrefix("/unstable").Subrouter()
|
||||||
|
|
||||||
v3mux.Handle("/createRoom",
|
v3mux.Handle("/createRoom",
|
||||||
|
@ -266,7 +313,7 @@ func Setup(
|
||||||
}),
|
}),
|
||||||
).Methods(http.MethodPost, http.MethodOptions)
|
).Methods(http.MethodPost, http.MethodOptions)
|
||||||
v3mux.Handle("/join/{roomIDOrAlias}",
|
v3mux.Handle("/join/{roomIDOrAlias}",
|
||||||
httputil.MakeAuthAPI(gomatrixserverlib.Join, userAPI, func(req *http.Request, device *userapi.Device) util.JSONResponse {
|
httputil.MakeAuthAPI(spec.Join, userAPI, func(req *http.Request, device *userapi.Device) util.JSONResponse {
|
||||||
if r := rateLimits.Limit(req, device); r != nil {
|
if r := rateLimits.Limit(req, device); r != nil {
|
||||||
return *r
|
return *r
|
||||||
}
|
}
|
||||||
|
@ -274,15 +321,23 @@ func Setup(
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return util.ErrorResponse(err)
|
return util.ErrorResponse(err)
|
||||||
}
|
}
|
||||||
return JoinRoomByIDOrAlias(
|
// Only execute a join for roomIDOrAlias and UserID once. If there is a join in progress
|
||||||
req, device, rsAPI, userAPI, vars["roomIDOrAlias"],
|
// it waits for it to complete and returns that result for subsequent requests.
|
||||||
)
|
resp, _, _ := sf.Do(vars["roomIDOrAlias"]+device.UserID, func() (any, error) {
|
||||||
|
return JoinRoomByIDOrAlias(
|
||||||
|
req, device, rsAPI, userAPI, vars["roomIDOrAlias"],
|
||||||
|
), nil
|
||||||
|
})
|
||||||
|
// once all joins are processed, drop them from the cache. Further requests
|
||||||
|
// will be processed as usual.
|
||||||
|
sf.Forget(vars["roomIDOrAlias"] + device.UserID)
|
||||||
|
return resp.(util.JSONResponse)
|
||||||
}, httputil.WithAllowGuests()),
|
}, httputil.WithAllowGuests()),
|
||||||
).Methods(http.MethodPost, http.MethodOptions)
|
).Methods(http.MethodPost, http.MethodOptions)
|
||||||
|
|
||||||
if mscCfg.Enabled("msc2753") {
|
if mscCfg.Enabled("msc2753") {
|
||||||
v3mux.Handle("/peek/{roomIDOrAlias}",
|
v3mux.Handle("/peek/{roomIDOrAlias}",
|
||||||
httputil.MakeAuthAPI(gomatrixserverlib.Peek, userAPI, func(req *http.Request, device *userapi.Device) util.JSONResponse {
|
httputil.MakeAuthAPI(spec.Peek, userAPI, func(req *http.Request, device *userapi.Device) util.JSONResponse {
|
||||||
if r := rateLimits.Limit(req, device); r != nil {
|
if r := rateLimits.Limit(req, device); r != nil {
|
||||||
return *r
|
return *r
|
||||||
}
|
}
|
||||||
|
@ -302,7 +357,7 @@ func Setup(
|
||||||
}, httputil.WithAllowGuests()),
|
}, httputil.WithAllowGuests()),
|
||||||
).Methods(http.MethodGet, http.MethodOptions)
|
).Methods(http.MethodGet, http.MethodOptions)
|
||||||
v3mux.Handle("/rooms/{roomID}/join",
|
v3mux.Handle("/rooms/{roomID}/join",
|
||||||
httputil.MakeAuthAPI(gomatrixserverlib.Join, userAPI, func(req *http.Request, device *userapi.Device) util.JSONResponse {
|
httputil.MakeAuthAPI(spec.Join, userAPI, func(req *http.Request, device *userapi.Device) util.JSONResponse {
|
||||||
if r := rateLimits.Limit(req, device); r != nil {
|
if r := rateLimits.Limit(req, device); r != nil {
|
||||||
return *r
|
return *r
|
||||||
}
|
}
|
||||||
|
@ -310,9 +365,17 @@ func Setup(
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return util.ErrorResponse(err)
|
return util.ErrorResponse(err)
|
||||||
}
|
}
|
||||||
return JoinRoomByIDOrAlias(
|
// Only execute a join for roomID and UserID once. If there is a join in progress
|
||||||
req, device, rsAPI, userAPI, vars["roomID"],
|
// it waits for it to complete and returns that result for subsequent requests.
|
||||||
)
|
resp, _, _ := sf.Do(vars["roomID"]+device.UserID, func() (any, error) {
|
||||||
|
return JoinRoomByIDOrAlias(
|
||||||
|
req, device, rsAPI, userAPI, vars["roomID"],
|
||||||
|
), nil
|
||||||
|
})
|
||||||
|
// once all joins are processed, drop them from the cache. Further requests
|
||||||
|
// will be processed as usual.
|
||||||
|
sf.Forget(vars["roomID"] + device.UserID)
|
||||||
|
return resp.(util.JSONResponse)
|
||||||
}, httputil.WithAllowGuests()),
|
}, httputil.WithAllowGuests()),
|
||||||
).Methods(http.MethodPost, http.MethodOptions)
|
).Methods(http.MethodPost, http.MethodOptions)
|
||||||
v3mux.Handle("/rooms/{roomID}/leave",
|
v3mux.Handle("/rooms/{roomID}/leave",
|
||||||
|
@ -459,6 +522,19 @@ func Setup(
|
||||||
}, httputil.WithAllowGuests()),
|
}, httputil.WithAllowGuests()),
|
||||||
).Methods(http.MethodPut, http.MethodOptions)
|
).Methods(http.MethodPut, http.MethodOptions)
|
||||||
|
|
||||||
|
// Defined outside of handler to persist between calls
|
||||||
|
// TODO: clear based on some criteria
|
||||||
|
roomHierarchyPaginationCache := NewRoomHierarchyPaginationCache()
|
||||||
|
v1mux.Handle("/rooms/{roomID}/hierarchy",
|
||||||
|
httputil.MakeAuthAPI("spaces", userAPI, func(req *http.Request, device *userapi.Device) util.JSONResponse {
|
||||||
|
vars, err := httputil.URLDecodeMapValues(mux.Vars(req))
|
||||||
|
if err != nil {
|
||||||
|
return util.ErrorResponse(err)
|
||||||
|
}
|
||||||
|
return QueryRoomHierarchy(req, device, vars["roomID"], rsAPI, &roomHierarchyPaginationCache)
|
||||||
|
}, httputil.WithAllowGuests()),
|
||||||
|
).Methods(http.MethodGet, http.MethodOptions)
|
||||||
|
|
||||||
v3mux.Handle("/register", httputil.MakeExternalAPI("register", func(req *http.Request) util.JSONResponse {
|
v3mux.Handle("/register", httputil.MakeExternalAPI("register", func(req *http.Request) util.JSONResponse {
|
||||||
if r := rateLimits.Limit(req, nil); r != nil {
|
if r := rateLimits.Limit(req, nil); r != nil {
|
||||||
return *r
|
return *r
|
||||||
|
@ -656,7 +732,7 @@ func Setup(
|
||||||
).Methods(http.MethodGet, http.MethodPost, http.MethodOptions)
|
).Methods(http.MethodGet, http.MethodPost, http.MethodOptions)
|
||||||
|
|
||||||
v3mux.Handle("/auth/{authType}/fallback/web",
|
v3mux.Handle("/auth/{authType}/fallback/web",
|
||||||
httputil.MakeHTMLAPI("auth_fallback", base.EnableMetrics, func(w http.ResponseWriter, req *http.Request) {
|
httputil.MakeHTMLAPI("auth_fallback", enableMetrics, func(w http.ResponseWriter, req *http.Request) {
|
||||||
vars := mux.Vars(req)
|
vars := mux.Vars(req)
|
||||||
AuthFallback(w, req, vars["authType"], cfg)
|
AuthFallback(w, req, vars["authType"], cfg)
|
||||||
}),
|
}),
|
||||||
|
@ -668,7 +744,7 @@ func Setup(
|
||||||
httputil.MakeAuthAPI("push_rules", userAPI, func(req *http.Request, device *userapi.Device) util.JSONResponse {
|
httputil.MakeAuthAPI("push_rules", userAPI, func(req *http.Request, device *userapi.Device) util.JSONResponse {
|
||||||
return util.JSONResponse{
|
return util.JSONResponse{
|
||||||
Code: http.StatusBadRequest,
|
Code: http.StatusBadRequest,
|
||||||
JSON: jsonerror.InvalidArgumentValue("missing trailing slash"),
|
JSON: spec.InvalidParam("missing trailing slash"),
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
).Methods(http.MethodGet, http.MethodOptions)
|
).Methods(http.MethodGet, http.MethodOptions)
|
||||||
|
@ -683,7 +759,7 @@ func Setup(
|
||||||
httputil.MakeAuthAPI("push_rules", userAPI, func(req *http.Request, device *userapi.Device) util.JSONResponse {
|
httputil.MakeAuthAPI("push_rules", userAPI, func(req *http.Request, device *userapi.Device) util.JSONResponse {
|
||||||
return util.JSONResponse{
|
return util.JSONResponse{
|
||||||
Code: http.StatusBadRequest,
|
Code: http.StatusBadRequest,
|
||||||
JSON: jsonerror.InvalidArgumentValue("scope, kind and rule ID must be specified"),
|
JSON: spec.InvalidParam("scope, kind and rule ID must be specified"),
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
).Methods(http.MethodPut)
|
).Methods(http.MethodPut)
|
||||||
|
@ -702,7 +778,7 @@ func Setup(
|
||||||
httputil.MakeAuthAPI("push_rules", userAPI, func(req *http.Request, device *userapi.Device) util.JSONResponse {
|
httputil.MakeAuthAPI("push_rules", userAPI, func(req *http.Request, device *userapi.Device) util.JSONResponse {
|
||||||
return util.JSONResponse{
|
return util.JSONResponse{
|
||||||
Code: http.StatusBadRequest,
|
Code: http.StatusBadRequest,
|
||||||
JSON: jsonerror.InvalidArgumentValue("missing trailing slash after scope"),
|
JSON: spec.InvalidParam("missing trailing slash after scope"),
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
).Methods(http.MethodGet, http.MethodOptions)
|
).Methods(http.MethodGet, http.MethodOptions)
|
||||||
|
@ -711,7 +787,7 @@ func Setup(
|
||||||
httputil.MakeAuthAPI("push_rules", userAPI, func(req *http.Request, device *userapi.Device) util.JSONResponse {
|
httputil.MakeAuthAPI("push_rules", userAPI, func(req *http.Request, device *userapi.Device) util.JSONResponse {
|
||||||
return util.JSONResponse{
|
return util.JSONResponse{
|
||||||
Code: http.StatusBadRequest,
|
Code: http.StatusBadRequest,
|
||||||
JSON: jsonerror.InvalidArgumentValue("kind and rule ID must be specified"),
|
JSON: spec.InvalidParam("kind and rule ID must be specified"),
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
).Methods(http.MethodPut)
|
).Methods(http.MethodPut)
|
||||||
|
@ -730,7 +806,7 @@ func Setup(
|
||||||
httputil.MakeAuthAPI("push_rules", userAPI, func(req *http.Request, device *userapi.Device) util.JSONResponse {
|
httputil.MakeAuthAPI("push_rules", userAPI, func(req *http.Request, device *userapi.Device) util.JSONResponse {
|
||||||
return util.JSONResponse{
|
return util.JSONResponse{
|
||||||
Code: http.StatusBadRequest,
|
Code: http.StatusBadRequest,
|
||||||
JSON: jsonerror.InvalidArgumentValue("missing trailing slash after kind"),
|
JSON: spec.InvalidParam("missing trailing slash after kind"),
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
).Methods(http.MethodGet, http.MethodOptions)
|
).Methods(http.MethodGet, http.MethodOptions)
|
||||||
|
@ -739,7 +815,7 @@ func Setup(
|
||||||
httputil.MakeAuthAPI("push_rules", userAPI, func(req *http.Request, device *userapi.Device) util.JSONResponse {
|
httputil.MakeAuthAPI("push_rules", userAPI, func(req *http.Request, device *userapi.Device) util.JSONResponse {
|
||||||
return util.JSONResponse{
|
return util.JSONResponse{
|
||||||
Code: http.StatusBadRequest,
|
Code: http.StatusBadRequest,
|
||||||
JSON: jsonerror.InvalidArgumentValue("rule ID must be specified"),
|
JSON: spec.InvalidParam("rule ID must be specified"),
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
).Methods(http.MethodPut)
|
).Methods(http.MethodPut)
|
||||||
|
@ -860,6 +936,8 @@ func Setup(
|
||||||
// Browsers use the OPTIONS HTTP method to check if the CORS policy allows
|
// Browsers use the OPTIONS HTTP method to check if the CORS policy allows
|
||||||
// PUT requests, so we need to allow this method
|
// PUT requests, so we need to allow this method
|
||||||
|
|
||||||
|
threePIDClient := base.CreateClient(dendriteCfg, nil) // TODO: Move this somewhere else, e.g. pass in as parameter
|
||||||
|
|
||||||
v3mux.Handle("/account/3pid",
|
v3mux.Handle("/account/3pid",
|
||||||
httputil.MakeAuthAPI("account_3pid", userAPI, func(req *http.Request, device *userapi.Device) util.JSONResponse {
|
httputil.MakeAuthAPI("account_3pid", userAPI, func(req *http.Request, device *userapi.Device) util.JSONResponse {
|
||||||
return GetAssociated3PIDs(req, userAPI, device)
|
return GetAssociated3PIDs(req, userAPI, device)
|
||||||
|
@ -868,11 +946,11 @@ func Setup(
|
||||||
|
|
||||||
v3mux.Handle("/account/3pid",
|
v3mux.Handle("/account/3pid",
|
||||||
httputil.MakeAuthAPI("account_3pid", userAPI, func(req *http.Request, device *userapi.Device) util.JSONResponse {
|
httputil.MakeAuthAPI("account_3pid", userAPI, func(req *http.Request, device *userapi.Device) util.JSONResponse {
|
||||||
return CheckAndSave3PIDAssociation(req, userAPI, device, cfg)
|
return CheckAndSave3PIDAssociation(req, userAPI, device, cfg, threePIDClient)
|
||||||
}),
|
}),
|
||||||
).Methods(http.MethodPost, http.MethodOptions)
|
).Methods(http.MethodPost, http.MethodOptions)
|
||||||
|
|
||||||
unstableMux.Handle("/account/3pid/delete",
|
v3mux.Handle("/account/3pid/delete",
|
||||||
httputil.MakeAuthAPI("account_3pid", userAPI, func(req *http.Request, device *userapi.Device) util.JSONResponse {
|
httputil.MakeAuthAPI("account_3pid", userAPI, func(req *http.Request, device *userapi.Device) util.JSONResponse {
|
||||||
return Forget3PID(req, userAPI)
|
return Forget3PID(req, userAPI)
|
||||||
}),
|
}),
|
||||||
|
@ -880,7 +958,7 @@ func Setup(
|
||||||
|
|
||||||
v3mux.Handle("/{path:(?:account/3pid|register)}/email/requestToken",
|
v3mux.Handle("/{path:(?:account/3pid|register)}/email/requestToken",
|
||||||
httputil.MakeExternalAPI("account_3pid_request_token", func(req *http.Request) util.JSONResponse {
|
httputil.MakeExternalAPI("account_3pid_request_token", func(req *http.Request) util.JSONResponse {
|
||||||
return RequestEmailToken(req, userAPI, cfg)
|
return RequestEmailToken(req, userAPI, cfg, threePIDClient)
|
||||||
}),
|
}),
|
||||||
).Methods(http.MethodPost, http.MethodOptions)
|
).Methods(http.MethodPost, http.MethodOptions)
|
||||||
|
|
||||||
|
@ -946,7 +1024,7 @@ func Setup(
|
||||||
// TODO: Allow people to peek into rooms.
|
// TODO: Allow people to peek into rooms.
|
||||||
return util.JSONResponse{
|
return util.JSONResponse{
|
||||||
Code: http.StatusForbidden,
|
Code: http.StatusForbidden,
|
||||||
JSON: jsonerror.GuestAccessForbidden("Guest access not implemented"),
|
JSON: spec.GuestAccessForbidden("Guest access not implemented"),
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
).Methods(http.MethodGet, http.MethodOptions)
|
).Methods(http.MethodGet, http.MethodOptions)
|
||||||
|
@ -1114,7 +1192,7 @@ func Setup(
|
||||||
|
|
||||||
v3mux.Handle("/delete_devices",
|
v3mux.Handle("/delete_devices",
|
||||||
httputil.MakeAuthAPI("delete_devices", userAPI, func(req *http.Request, device *userapi.Device) util.JSONResponse {
|
httputil.MakeAuthAPI("delete_devices", userAPI, func(req *http.Request, device *userapi.Device) util.JSONResponse {
|
||||||
return DeleteDevices(req, userAPI, device)
|
return DeleteDevices(req, userInteractiveAuth, userAPI, device)
|
||||||
}),
|
}),
|
||||||
).Methods(http.MethodPost, http.MethodOptions)
|
).Methods(http.MethodPost, http.MethodOptions)
|
||||||
|
|
||||||
|
@ -1193,7 +1271,7 @@ func Setup(
|
||||||
if r := rateLimits.Limit(req, device); r != nil {
|
if r := rateLimits.Limit(req, device); r != nil {
|
||||||
return *r
|
return *r
|
||||||
}
|
}
|
||||||
return GetCapabilities(req, rsAPI)
|
return GetCapabilities(rsAPI)
|
||||||
}, httputil.WithAllowGuests()),
|
}, httputil.WithAllowGuests()),
|
||||||
).Methods(http.MethodGet, http.MethodOptions)
|
).Methods(http.MethodGet, http.MethodOptions)
|
||||||
|
|
||||||
|
@ -1251,7 +1329,7 @@ func Setup(
|
||||||
if version == "" {
|
if version == "" {
|
||||||
return util.JSONResponse{
|
return util.JSONResponse{
|
||||||
Code: 400,
|
Code: 400,
|
||||||
JSON: jsonerror.InvalidArgumentValue("version must be specified"),
|
JSON: spec.InvalidParam("version must be specified"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
var reqBody keyBackupSessionRequest
|
var reqBody keyBackupSessionRequest
|
||||||
|
@ -1272,7 +1350,7 @@ func Setup(
|
||||||
if version == "" {
|
if version == "" {
|
||||||
return util.JSONResponse{
|
return util.JSONResponse{
|
||||||
Code: 400,
|
Code: 400,
|
||||||
JSON: jsonerror.InvalidArgumentValue("version must be specified"),
|
JSON: spec.InvalidParam("version must be specified"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
roomID := vars["roomID"]
|
roomID := vars["roomID"]
|
||||||
|
@ -1304,7 +1382,7 @@ func Setup(
|
||||||
if version == "" {
|
if version == "" {
|
||||||
return util.JSONResponse{
|
return util.JSONResponse{
|
||||||
Code: 400,
|
Code: 400,
|
||||||
JSON: jsonerror.InvalidArgumentValue("version must be specified"),
|
JSON: spec.InvalidParam("version must be specified"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
var reqBody userapi.KeyBackupSession
|
var reqBody userapi.KeyBackupSession
|
||||||
|
@ -1370,7 +1448,7 @@ func Setup(
|
||||||
// Cross-signing device keys
|
// Cross-signing device keys
|
||||||
|
|
||||||
postDeviceSigningKeys := httputil.MakeAuthAPI("post_device_signing_keys", userAPI, func(req *http.Request, device *userapi.Device) util.JSONResponse {
|
postDeviceSigningKeys := httputil.MakeAuthAPI("post_device_signing_keys", userAPI, func(req *http.Request, device *userapi.Device) util.JSONResponse {
|
||||||
return UploadCrossSigningDeviceKeys(req, userInteractiveAuth, userAPI, device, userAPI, cfg)
|
return UploadCrossSigningDeviceKeys(req, userAPI, device, userAPI, cfg)
|
||||||
})
|
})
|
||||||
|
|
||||||
postDeviceSigningSignatures := httputil.MakeAuthAPI("post_device_signing_signatures", userAPI, func(req *http.Request, device *userapi.Device) util.JSONResponse {
|
postDeviceSigningSignatures := httputil.MakeAuthAPI("post_device_signing_signatures", userAPI, func(req *http.Request, device *userapi.Device) util.JSONResponse {
|
||||||
|
@ -1405,7 +1483,7 @@ func Setup(
|
||||||
}, httputil.WithAllowGuests()),
|
}, httputil.WithAllowGuests()),
|
||||||
).Methods(http.MethodPost, http.MethodOptions)
|
).Methods(http.MethodPost, http.MethodOptions)
|
||||||
v3mux.Handle("/rooms/{roomId}/receipt/{receiptType}/{eventId}",
|
v3mux.Handle("/rooms/{roomId}/receipt/{receiptType}/{eventId}",
|
||||||
httputil.MakeAuthAPI(gomatrixserverlib.Join, userAPI, func(req *http.Request, device *userapi.Device) util.JSONResponse {
|
httputil.MakeAuthAPI(spec.Join, userAPI, func(req *http.Request, device *userapi.Device) util.JSONResponse {
|
||||||
if r := rateLimits.Limit(req, device); r != nil {
|
if r := rateLimits.Limit(req, device); r != nil {
|
||||||
return *r
|
return *r
|
||||||
}
|
}
|
||||||
|
@ -1435,4 +1513,58 @@ func Setup(
|
||||||
return GetPresence(req, device, natsClient, cfg.Matrix.JetStream.Prefixed(jetstream.RequestPresence), vars["userId"])
|
return GetPresence(req, device, natsClient, cfg.Matrix.JetStream.Prefixed(jetstream.RequestPresence), vars["userId"])
|
||||||
}),
|
}),
|
||||||
).Methods(http.MethodGet, http.MethodOptions)
|
).Methods(http.MethodGet, http.MethodOptions)
|
||||||
|
|
||||||
|
v3mux.Handle("/rooms/{roomID}/joined_members",
|
||||||
|
httputil.MakeAuthAPI("rooms_members", userAPI, func(req *http.Request, device *userapi.Device) util.JSONResponse {
|
||||||
|
vars, err := httputil.URLDecodeMapValues(mux.Vars(req))
|
||||||
|
if err != nil {
|
||||||
|
return util.ErrorResponse(err)
|
||||||
|
}
|
||||||
|
return GetJoinedMembers(req, device, vars["roomID"], rsAPI)
|
||||||
|
}),
|
||||||
|
).Methods(http.MethodGet, http.MethodOptions)
|
||||||
|
|
||||||
|
v3mux.Handle("/rooms/{roomID}/report/{eventID}",
|
||||||
|
httputil.MakeAuthAPI("report_event", userAPI, func(req *http.Request, device *userapi.Device) util.JSONResponse {
|
||||||
|
vars, err := httputil.URLDecodeMapValues(mux.Vars(req))
|
||||||
|
if err != nil {
|
||||||
|
return util.ErrorResponse(err)
|
||||||
|
}
|
||||||
|
return ReportEvent(req, device, vars["roomID"], vars["eventID"], rsAPI)
|
||||||
|
}),
|
||||||
|
).Methods(http.MethodPost, http.MethodOptions)
|
||||||
|
|
||||||
|
synapseAdminRouter.Handle("/admin/v1/event_reports",
|
||||||
|
httputil.MakeAdminAPI("admin_report_events", userAPI, func(req *http.Request, device *userapi.Device) util.JSONResponse {
|
||||||
|
from := parseUint64OrDefault(req.URL.Query().Get("from"), 0)
|
||||||
|
limit := parseUint64OrDefault(req.URL.Query().Get("limit"), 100)
|
||||||
|
dir := req.URL.Query().Get("dir")
|
||||||
|
userID := req.URL.Query().Get("user_id")
|
||||||
|
roomID := req.URL.Query().Get("room_id")
|
||||||
|
|
||||||
|
// Go backwards if direction is empty or "b"
|
||||||
|
backwards := dir == "" || dir == "b"
|
||||||
|
return GetEventReports(req, rsAPI, from, limit, backwards, userID, roomID)
|
||||||
|
}),
|
||||||
|
).Methods(http.MethodGet, http.MethodOptions)
|
||||||
|
|
||||||
|
synapseAdminRouter.Handle("/admin/v1/event_reports/{reportID}",
|
||||||
|
httputil.MakeAdminAPI("admin_report_event", userAPI, func(req *http.Request, device *userapi.Device) util.JSONResponse {
|
||||||
|
vars, err := httputil.URLDecodeMapValues(mux.Vars(req))
|
||||||
|
if err != nil {
|
||||||
|
return util.ErrorResponse(err)
|
||||||
|
}
|
||||||
|
return GetEventReport(req, rsAPI, vars["reportID"])
|
||||||
|
}),
|
||||||
|
).Methods(http.MethodGet, http.MethodOptions)
|
||||||
|
|
||||||
|
synapseAdminRouter.Handle("/admin/v1/event_reports/{reportID}",
|
||||||
|
httputil.MakeAdminAPI("admin_report_event_delete", userAPI, func(req *http.Request, device *userapi.Device) util.JSONResponse {
|
||||||
|
vars, err := httputil.URLDecodeMapValues(mux.Vars(req))
|
||||||
|
if err != nil {
|
||||||
|
return util.ErrorResponse(err)
|
||||||
|
}
|
||||||
|
return DeleteEventReport(req, rsAPI, vars["reportID"])
|
||||||
|
}),
|
||||||
|
).Methods(http.MethodDelete, http.MethodOptions)
|
||||||
}
|
}
|
||||||
|
|
|
@ -23,18 +23,19 @@ import (
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/matrix-org/gomatrixserverlib"
|
|
||||||
"github.com/matrix-org/util"
|
|
||||||
"github.com/prometheus/client_golang/prometheus"
|
|
||||||
"github.com/sirupsen/logrus"
|
|
||||||
|
|
||||||
"github.com/matrix-org/dendrite/clientapi/httputil"
|
"github.com/matrix-org/dendrite/clientapi/httputil"
|
||||||
"github.com/matrix-org/dendrite/clientapi/jsonerror"
|
|
||||||
"github.com/matrix-org/dendrite/internal/eventutil"
|
"github.com/matrix-org/dendrite/internal/eventutil"
|
||||||
"github.com/matrix-org/dendrite/internal/transactions"
|
"github.com/matrix-org/dendrite/internal/transactions"
|
||||||
"github.com/matrix-org/dendrite/roomserver/api"
|
"github.com/matrix-org/dendrite/roomserver/api"
|
||||||
|
"github.com/matrix-org/dendrite/roomserver/types"
|
||||||
"github.com/matrix-org/dendrite/setup/config"
|
"github.com/matrix-org/dendrite/setup/config"
|
||||||
|
"github.com/matrix-org/dendrite/syncapi/synctypes"
|
||||||
userapi "github.com/matrix-org/dendrite/userapi/api"
|
userapi "github.com/matrix-org/dendrite/userapi/api"
|
||||||
|
"github.com/matrix-org/gomatrixserverlib"
|
||||||
|
"github.com/matrix-org/gomatrixserverlib/spec"
|
||||||
|
"github.com/matrix-org/util"
|
||||||
|
"github.com/prometheus/client_golang/prometheus"
|
||||||
|
"github.com/sirupsen/logrus"
|
||||||
)
|
)
|
||||||
|
|
||||||
// http://matrix.org/docs/spec/client_server/r0.2.0.html#put-matrix-client-r0-rooms-roomid-send-eventtype-txnid
|
// http://matrix.org/docs/spec/client_server/r0.2.0.html#put-matrix-client-r0-rooms-roomid-send-eventtype-txnid
|
||||||
|
@ -67,6 +68,8 @@ var sendEventDuration = prometheus.NewHistogramVec(
|
||||||
// /rooms/{roomID}/send/{eventType}
|
// /rooms/{roomID}/send/{eventType}
|
||||||
// /rooms/{roomID}/send/{eventType}/{txnID}
|
// /rooms/{roomID}/send/{eventType}/{txnID}
|
||||||
// /rooms/{roomID}/state/{eventType}/{stateKey}
|
// /rooms/{roomID}/state/{eventType}/{stateKey}
|
||||||
|
//
|
||||||
|
// nolint: gocyclo
|
||||||
func SendEvent(
|
func SendEvent(
|
||||||
req *http.Request,
|
req *http.Request,
|
||||||
device *userapi.Device,
|
device *userapi.Device,
|
||||||
|
@ -75,12 +78,11 @@ func SendEvent(
|
||||||
rsAPI api.ClientRoomserverAPI,
|
rsAPI api.ClientRoomserverAPI,
|
||||||
txnCache *transactions.Cache,
|
txnCache *transactions.Cache,
|
||||||
) util.JSONResponse {
|
) util.JSONResponse {
|
||||||
verReq := api.QueryRoomVersionForRoomRequest{RoomID: roomID}
|
roomVersion, err := rsAPI.QueryRoomVersionForRoom(req.Context(), roomID)
|
||||||
verRes := api.QueryRoomVersionForRoomResponse{}
|
if err != nil {
|
||||||
if err := rsAPI.QueryRoomVersionForRoom(req.Context(), &verReq, &verRes); err != nil {
|
|
||||||
return util.JSONResponse{
|
return util.JSONResponse{
|
||||||
Code: http.StatusBadRequest,
|
Code: http.StatusBadRequest,
|
||||||
JSON: jsonerror.UnsupportedRoomVersion(err.Error()),
|
JSON: spec.UnsupportedRoomVersion(err.Error()),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -91,6 +93,30 @@ func SendEvent(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Translate user ID state keys to room keys in pseudo ID rooms
|
||||||
|
if roomVersion == gomatrixserverlib.RoomVersionPseudoIDs && stateKey != nil {
|
||||||
|
parsedRoomID, innerErr := spec.NewRoomID(roomID)
|
||||||
|
if innerErr != nil {
|
||||||
|
return util.JSONResponse{
|
||||||
|
Code: http.StatusBadRequest,
|
||||||
|
JSON: spec.InvalidParam("invalid room ID"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
newStateKey, innerErr := synctypes.FromClientStateKey(*parsedRoomID, *stateKey, func(roomID spec.RoomID, userID spec.UserID) (*spec.SenderID, error) {
|
||||||
|
return rsAPI.QuerySenderIDForUser(req.Context(), roomID, userID)
|
||||||
|
})
|
||||||
|
if innerErr != nil {
|
||||||
|
// TODO: work out better logic for failure cases (e.g. sender ID not found)
|
||||||
|
util.GetLogger(req.Context()).WithError(innerErr).Error("synctypes.FromClientStateKey failed")
|
||||||
|
return util.JSONResponse{
|
||||||
|
Code: http.StatusInternalServerError,
|
||||||
|
JSON: spec.Unknown("internal server error"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
stateKey = newStateKey
|
||||||
|
}
|
||||||
|
|
||||||
// create a mutex for the specific user in the specific room
|
// create a mutex for the specific user in the specific room
|
||||||
// this avoids a situation where events that are received in quick succession are sent to the roomserver in a jumbled order
|
// this avoids a situation where events that are received in quick succession are sent to the roomserver in a jumbled order
|
||||||
userID := device.UserID
|
userID := device.UserID
|
||||||
|
@ -117,26 +143,37 @@ func SendEvent(
|
||||||
// If we're sending a membership update, make sure to strip the authorised
|
// If we're sending a membership update, make sure to strip the authorised
|
||||||
// via key if it is present, otherwise other servers won't be able to auth
|
// via key if it is present, otherwise other servers won't be able to auth
|
||||||
// the event if the room is set to the "restricted" join rule.
|
// the event if the room is set to the "restricted" join rule.
|
||||||
if eventType == gomatrixserverlib.MRoomMember {
|
if eventType == spec.MRoomMember {
|
||||||
delete(r, "join_authorised_via_users_server")
|
delete(r, "join_authorised_via_users_server")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// for power level events we need to replace the userID with the pseudoID
|
||||||
|
if roomVersion == gomatrixserverlib.RoomVersionPseudoIDs && eventType == spec.MRoomPowerLevels {
|
||||||
|
err = updatePowerLevels(req, r, roomID, rsAPI)
|
||||||
|
if err != nil {
|
||||||
|
return util.JSONResponse{
|
||||||
|
Code: http.StatusInternalServerError,
|
||||||
|
JSON: spec.InternalServerError{Err: err.Error()},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
evTime, err := httputil.ParseTSParam(req)
|
evTime, err := httputil.ParseTSParam(req)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return util.JSONResponse{
|
return util.JSONResponse{
|
||||||
Code: http.StatusBadRequest,
|
Code: http.StatusBadRequest,
|
||||||
JSON: jsonerror.InvalidArgumentValue(err.Error()),
|
JSON: spec.InvalidParam(err.Error()),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
e, resErr := generateSendEvent(req.Context(), r, device, roomID, eventType, stateKey, cfg, rsAPI, evTime)
|
e, resErr := generateSendEvent(req.Context(), r, device, roomID, eventType, stateKey, rsAPI, evTime)
|
||||||
if resErr != nil {
|
if resErr != nil {
|
||||||
return *resErr
|
return *resErr
|
||||||
}
|
}
|
||||||
timeToGenerateEvent := time.Since(startedGeneratingEvent)
|
timeToGenerateEvent := time.Since(startedGeneratingEvent)
|
||||||
|
|
||||||
// validate that the aliases exists
|
// validate that the aliases exists
|
||||||
if eventType == gomatrixserverlib.MRoomCanonicalAlias && stateKey != nil && *stateKey == "" {
|
if eventType == spec.MRoomCanonicalAlias && stateKey != nil && *stateKey == "" {
|
||||||
aliasReq := api.AliasEvent{}
|
aliasReq := api.AliasEvent{}
|
||||||
if err = json.Unmarshal(e.Content(), &aliasReq); err != nil {
|
if err = json.Unmarshal(e.Content(), &aliasReq); err != nil {
|
||||||
return util.ErrorResponse(fmt.Errorf("unable to parse alias event: %w", err))
|
return util.ErrorResponse(fmt.Errorf("unable to parse alias event: %w", err))
|
||||||
|
@ -144,12 +181,15 @@ func SendEvent(
|
||||||
if !aliasReq.Valid() {
|
if !aliasReq.Valid() {
|
||||||
return util.JSONResponse{
|
return util.JSONResponse{
|
||||||
Code: http.StatusBadRequest,
|
Code: http.StatusBadRequest,
|
||||||
JSON: jsonerror.InvalidParam("Request contains invalid aliases."),
|
JSON: spec.InvalidParam("Request contains invalid aliases."),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
aliasRes := &api.GetAliasesForRoomIDResponse{}
|
aliasRes := &api.GetAliasesForRoomIDResponse{}
|
||||||
if err = rsAPI.GetAliasesForRoomID(req.Context(), &api.GetAliasesForRoomIDRequest{RoomID: roomID}, aliasRes); err != nil {
|
if err = rsAPI.GetAliasesForRoomID(req.Context(), &api.GetAliasesForRoomIDRequest{RoomID: roomID}, aliasRes); err != nil {
|
||||||
return jsonerror.InternalServerError()
|
return util.JSONResponse{
|
||||||
|
Code: http.StatusInternalServerError,
|
||||||
|
JSON: spec.InternalServerError{},
|
||||||
|
}
|
||||||
}
|
}
|
||||||
var found int
|
var found int
|
||||||
requestAliases := append(aliasReq.AltAliases, aliasReq.Alias)
|
requestAliases := append(aliasReq.AltAliases, aliasReq.Alias)
|
||||||
|
@ -164,7 +204,7 @@ func SendEvent(
|
||||||
if aliasReq.Alias != "" && found < len(requestAliases) {
|
if aliasReq.Alias != "" && found < len(requestAliases) {
|
||||||
return util.JSONResponse{
|
return util.JSONResponse{
|
||||||
Code: http.StatusBadRequest,
|
Code: http.StatusBadRequest,
|
||||||
JSON: jsonerror.BadAlias("No matching alias found."),
|
JSON: spec.BadAlias("No matching alias found."),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -183,8 +223,8 @@ func SendEvent(
|
||||||
if err := api.SendEvents(
|
if err := api.SendEvents(
|
||||||
req.Context(), rsAPI,
|
req.Context(), rsAPI,
|
||||||
api.KindNew,
|
api.KindNew,
|
||||||
[]*gomatrixserverlib.HeaderedEvent{
|
[]*types.HeaderedEvent{
|
||||||
e.Headered(verRes.RoomVersion),
|
{PDU: e},
|
||||||
},
|
},
|
||||||
device.UserDomain(),
|
device.UserDomain(),
|
||||||
domain,
|
domain,
|
||||||
|
@ -193,13 +233,16 @@ func SendEvent(
|
||||||
false,
|
false,
|
||||||
); err != nil {
|
); err != nil {
|
||||||
util.GetLogger(req.Context()).WithError(err).Error("SendEvents failed")
|
util.GetLogger(req.Context()).WithError(err).Error("SendEvents failed")
|
||||||
return jsonerror.InternalServerError()
|
return util.JSONResponse{
|
||||||
|
Code: http.StatusInternalServerError,
|
||||||
|
JSON: spec.InternalServerError{},
|
||||||
|
}
|
||||||
}
|
}
|
||||||
timeToSubmitEvent := time.Since(startedSubmittingEvent)
|
timeToSubmitEvent := time.Since(startedSubmittingEvent)
|
||||||
util.GetLogger(req.Context()).WithFields(logrus.Fields{
|
util.GetLogger(req.Context()).WithFields(logrus.Fields{
|
||||||
"event_id": e.EventID(),
|
"event_id": e.EventID(),
|
||||||
"room_id": roomID,
|
"room_id": roomID,
|
||||||
"room_version": verRes.RoomVersion,
|
"room_version": roomVersion,
|
||||||
}).Info("Sent event to roomserver")
|
}).Info("Sent event to roomserver")
|
||||||
|
|
||||||
res := util.JSONResponse{
|
res := util.JSONResponse{
|
||||||
|
@ -219,6 +262,35 @@ func SendEvent(
|
||||||
return res
|
return res
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func updatePowerLevels(req *http.Request, r map[string]interface{}, roomID string, rsAPI api.ClientRoomserverAPI) error {
|
||||||
|
users, ok := r["users"]
|
||||||
|
if !ok {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
userMap := users.(map[string]interface{})
|
||||||
|
validRoomID, err := spec.NewRoomID(roomID)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
for user, level := range userMap {
|
||||||
|
uID, err := spec.NewUserID(user, true)
|
||||||
|
if err != nil {
|
||||||
|
continue // we're modifying the map in place, so we're going to have invalid userIDs after the first iteration
|
||||||
|
}
|
||||||
|
senderID, err := rsAPI.QuerySenderIDForUser(req.Context(), *validRoomID, *uID)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
} else if senderID == nil {
|
||||||
|
util.GetLogger(req.Context()).Warnf("sender ID not found for %s in %s", uID, *validRoomID)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
userMap[string(*senderID)] = level
|
||||||
|
delete(userMap, user)
|
||||||
|
}
|
||||||
|
r["users"] = userMap
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
// stateEqual compares the new and the existing state event content. If they are equal, returns a *util.JSONResponse
|
// stateEqual compares the new and the existing state event content. If they are equal, returns a *util.JSONResponse
|
||||||
// with the existing event_id, making this an idempotent request.
|
// with the existing event_id, making this an idempotent request.
|
||||||
func stateEqual(ctx context.Context, rsAPI api.ClientRoomserverAPI, eventType, stateKey, roomID string, newContent map[string]interface{}) *util.JSONResponse {
|
func stateEqual(ctx context.Context, rsAPI api.ClientRoomserverAPI, eventType, stateKey, roomID string, newContent map[string]interface{}) *util.JSONResponse {
|
||||||
|
@ -255,72 +327,108 @@ func generateSendEvent(
|
||||||
r map[string]interface{},
|
r map[string]interface{},
|
||||||
device *userapi.Device,
|
device *userapi.Device,
|
||||||
roomID, eventType string, stateKey *string,
|
roomID, eventType string, stateKey *string,
|
||||||
cfg *config.ClientAPI,
|
|
||||||
rsAPI api.ClientRoomserverAPI,
|
rsAPI api.ClientRoomserverAPI,
|
||||||
evTime time.Time,
|
evTime time.Time,
|
||||||
) (*gomatrixserverlib.Event, *util.JSONResponse) {
|
) (gomatrixserverlib.PDU, *util.JSONResponse) {
|
||||||
// parse the incoming http request
|
// parse the incoming http request
|
||||||
userID := device.UserID
|
fullUserID, err := spec.NewUserID(device.UserID, true)
|
||||||
|
if err != nil {
|
||||||
|
return nil, &util.JSONResponse{
|
||||||
|
Code: http.StatusBadRequest,
|
||||||
|
JSON: spec.BadJSON("Bad userID"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
validRoomID, err := spec.NewRoomID(roomID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, &util.JSONResponse{
|
||||||
|
Code: http.StatusBadRequest,
|
||||||
|
JSON: spec.BadJSON("RoomID is invalid"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
senderID, err := rsAPI.QuerySenderIDForUser(ctx, *validRoomID, *fullUserID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, &util.JSONResponse{
|
||||||
|
Code: http.StatusInternalServerError,
|
||||||
|
JSON: spec.NotFound("internal server error"),
|
||||||
|
}
|
||||||
|
} else if senderID == nil {
|
||||||
|
// TODO: is it always the case that lack of a sender ID means they're not joined?
|
||||||
|
// And should this logic be deferred to the roomserver somehow?
|
||||||
|
return nil, &util.JSONResponse{
|
||||||
|
Code: http.StatusForbidden,
|
||||||
|
JSON: spec.Forbidden("not joined to room"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// create the new event and set all the fields we can
|
// create the new event and set all the fields we can
|
||||||
builder := gomatrixserverlib.EventBuilder{
|
proto := gomatrixserverlib.ProtoEvent{
|
||||||
Sender: userID,
|
SenderID: string(*senderID),
|
||||||
RoomID: roomID,
|
RoomID: roomID,
|
||||||
Type: eventType,
|
Type: eventType,
|
||||||
StateKey: stateKey,
|
StateKey: stateKey,
|
||||||
}
|
}
|
||||||
err := builder.SetContent(r)
|
err = proto.SetContent(r)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
util.GetLogger(ctx).WithError(err).Error("builder.SetContent failed")
|
util.GetLogger(ctx).WithError(err).Error("proto.SetContent failed")
|
||||||
resErr := jsonerror.InternalServerError()
|
return nil, &util.JSONResponse{
|
||||||
return nil, &resErr
|
Code: http.StatusInternalServerError,
|
||||||
|
JSON: spec.InternalServerError{},
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
identity, err := cfg.Matrix.SigningIdentityFor(device.UserDomain())
|
identity, err := rsAPI.SigningIdentityFor(ctx, *validRoomID, *fullUserID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
resErr := jsonerror.InternalServerError()
|
return nil, &util.JSONResponse{
|
||||||
return nil, &resErr
|
Code: http.StatusInternalServerError,
|
||||||
|
JSON: spec.InternalServerError{},
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var queryRes api.QueryLatestEventsAndStateResponse
|
var queryRes api.QueryLatestEventsAndStateResponse
|
||||||
e, err := eventutil.QueryAndBuildEvent(ctx, &builder, cfg.Matrix, identity, evTime, rsAPI, &queryRes)
|
e, err := eventutil.QueryAndBuildEvent(ctx, &proto, &identity, evTime, rsAPI, &queryRes)
|
||||||
if err == eventutil.ErrRoomNoExists {
|
switch specificErr := err.(type) {
|
||||||
|
case nil:
|
||||||
|
case eventutil.ErrRoomNoExists:
|
||||||
return nil, &util.JSONResponse{
|
return nil, &util.JSONResponse{
|
||||||
Code: http.StatusNotFound,
|
Code: http.StatusNotFound,
|
||||||
JSON: jsonerror.NotFound("Room does not exist"),
|
JSON: spec.NotFound("Room does not exist"),
|
||||||
}
|
}
|
||||||
} else if e, ok := err.(gomatrixserverlib.BadJSONError); ok {
|
case gomatrixserverlib.BadJSONError:
|
||||||
return nil, &util.JSONResponse{
|
return nil, &util.JSONResponse{
|
||||||
Code: http.StatusBadRequest,
|
Code: http.StatusBadRequest,
|
||||||
JSON: jsonerror.BadJSON(e.Error()),
|
JSON: spec.BadJSON(specificErr.Error()),
|
||||||
}
|
}
|
||||||
} else if e, ok := err.(gomatrixserverlib.EventValidationError); ok {
|
case gomatrixserverlib.EventValidationError:
|
||||||
if e.Code == gomatrixserverlib.EventValidationTooLarge {
|
if specificErr.Code == gomatrixserverlib.EventValidationTooLarge {
|
||||||
return nil, &util.JSONResponse{
|
return nil, &util.JSONResponse{
|
||||||
Code: http.StatusRequestEntityTooLarge,
|
Code: http.StatusRequestEntityTooLarge,
|
||||||
JSON: jsonerror.BadJSON(e.Error()),
|
JSON: spec.BadJSON(specificErr.Error()),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return nil, &util.JSONResponse{
|
return nil, &util.JSONResponse{
|
||||||
Code: http.StatusBadRequest,
|
Code: http.StatusBadRequest,
|
||||||
JSON: jsonerror.BadJSON(e.Error()),
|
JSON: spec.BadJSON(specificErr.Error()),
|
||||||
}
|
}
|
||||||
} else if err != nil {
|
default:
|
||||||
util.GetLogger(ctx).WithError(err).Error("eventutil.BuildEvent failed")
|
util.GetLogger(ctx).WithError(err).Error("eventutil.BuildEvent failed")
|
||||||
resErr := jsonerror.InternalServerError()
|
return nil, &util.JSONResponse{
|
||||||
return nil, &resErr
|
Code: http.StatusInternalServerError,
|
||||||
|
JSON: spec.InternalServerError{},
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// check to see if this user can perform this operation
|
// check to see if this user can perform this operation
|
||||||
stateEvents := make([]*gomatrixserverlib.Event, len(queryRes.StateEvents))
|
stateEvents := make([]gomatrixserverlib.PDU, len(queryRes.StateEvents))
|
||||||
for i := range queryRes.StateEvents {
|
for i := range queryRes.StateEvents {
|
||||||
stateEvents[i] = queryRes.StateEvents[i].Event
|
stateEvents[i] = queryRes.StateEvents[i].PDU
|
||||||
}
|
}
|
||||||
provider := gomatrixserverlib.NewAuthEvents(stateEvents)
|
provider := gomatrixserverlib.NewAuthEvents(gomatrixserverlib.ToPDUs(stateEvents))
|
||||||
if err = gomatrixserverlib.Allowed(e.Event, &provider); err != nil {
|
if err = gomatrixserverlib.Allowed(e.PDU, &provider, func(roomID spec.RoomID, senderID spec.SenderID) (*spec.UserID, error) {
|
||||||
|
return rsAPI.QueryUserIDForSender(ctx, *validRoomID, senderID)
|
||||||
|
}); err != nil {
|
||||||
return nil, &util.JSONResponse{
|
return nil, &util.JSONResponse{
|
||||||
Code: http.StatusForbidden,
|
Code: http.StatusForbidden,
|
||||||
JSON: jsonerror.Forbidden(err.Error()), // TODO: Is this error string comprehensible to the client?
|
JSON: spec.Forbidden(err.Error()), // TODO: Is this error string comprehensible to the client?
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -331,16 +439,16 @@ func generateSendEvent(
|
||||||
util.GetLogger(ctx).WithError(err).Error("Cannot unmarshal the event content.")
|
util.GetLogger(ctx).WithError(err).Error("Cannot unmarshal the event content.")
|
||||||
return nil, &util.JSONResponse{
|
return nil, &util.JSONResponse{
|
||||||
Code: http.StatusBadRequest,
|
Code: http.StatusBadRequest,
|
||||||
JSON: jsonerror.BadJSON("Cannot unmarshal the event content."),
|
JSON: spec.BadJSON("Cannot unmarshal the event content."),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if content["replacement_room"] == e.RoomID() {
|
if content["replacement_room"] == e.RoomID().String() {
|
||||||
return nil, &util.JSONResponse{
|
return nil, &util.JSONResponse{
|
||||||
Code: http.StatusBadRequest,
|
Code: http.StatusBadRequest,
|
||||||
JSON: jsonerror.InvalidParam("Cannot send tombstone event that points to the same room."),
|
JSON: spec.InvalidParam("Cannot send tombstone event that points to the same room."),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return e.Event, nil
|
return e.PDU, nil
|
||||||
}
|
}
|
||||||
|
|
275
clientapi/routing/sendevent_test.go
Normal file
275
clientapi/routing/sendevent_test.go
Normal file
|
@ -0,0 +1,275 @@
|
||||||
|
package routing
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"crypto/ed25519"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
rsapi "github.com/matrix-org/dendrite/roomserver/api"
|
||||||
|
"github.com/matrix-org/dendrite/roomserver/types"
|
||||||
|
"github.com/matrix-org/dendrite/setup/config"
|
||||||
|
uapi "github.com/matrix-org/dendrite/userapi/api"
|
||||||
|
"github.com/matrix-org/gomatrixserverlib"
|
||||||
|
"github.com/matrix-org/gomatrixserverlib/fclient"
|
||||||
|
"github.com/matrix-org/gomatrixserverlib/spec"
|
||||||
|
"gotest.tools/v3/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Mock roomserver API for testing
|
||||||
|
//
|
||||||
|
// Currently pretty specialised for the pseudo ID test, so will need
|
||||||
|
// editing if future (other) sendevent tests are using this.
|
||||||
|
type sendEventTestRoomserverAPI struct {
|
||||||
|
rsapi.ClientRoomserverAPI
|
||||||
|
t *testing.T
|
||||||
|
roomIDStr string
|
||||||
|
roomVersion gomatrixserverlib.RoomVersion
|
||||||
|
roomState []*types.HeaderedEvent
|
||||||
|
|
||||||
|
// userID -> room key
|
||||||
|
senderMapping map[string]ed25519.PrivateKey
|
||||||
|
|
||||||
|
savedInputRoomEvents []rsapi.InputRoomEvent
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *sendEventTestRoomserverAPI) QueryRoomVersionForRoom(ctx context.Context, roomID string) (gomatrixserverlib.RoomVersion, error) {
|
||||||
|
if roomID == s.roomIDStr {
|
||||||
|
return s.roomVersion, nil
|
||||||
|
} else {
|
||||||
|
s.t.Logf("room version queried for %s", roomID)
|
||||||
|
return "", fmt.Errorf("unknown room")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *sendEventTestRoomserverAPI) QueryCurrentState(ctx context.Context, req *rsapi.QueryCurrentStateRequest, res *rsapi.QueryCurrentStateResponse) error {
|
||||||
|
res.StateEvents = map[gomatrixserverlib.StateKeyTuple]*types.HeaderedEvent{}
|
||||||
|
for _, stateKeyTuple := range req.StateTuples {
|
||||||
|
for _, stateEv := range s.roomState {
|
||||||
|
if stateEv.Type() == stateKeyTuple.EventType && stateEv.StateKey() != nil && *stateEv.StateKey() == stateKeyTuple.StateKey {
|
||||||
|
res.StateEvents[stateKeyTuple] = stateEv
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *sendEventTestRoomserverAPI) QueryLatestEventsAndState(ctx context.Context, req *rsapi.QueryLatestEventsAndStateRequest, res *rsapi.QueryLatestEventsAndStateResponse) error {
|
||||||
|
if req.RoomID == s.roomIDStr {
|
||||||
|
res.RoomExists = true
|
||||||
|
res.RoomVersion = s.roomVersion
|
||||||
|
|
||||||
|
res.StateEvents = make([]*types.HeaderedEvent, len(s.roomState))
|
||||||
|
copy(res.StateEvents, s.roomState)
|
||||||
|
|
||||||
|
res.LatestEvents = []string{}
|
||||||
|
res.Depth = 1
|
||||||
|
return nil
|
||||||
|
} else {
|
||||||
|
s.t.Logf("room event/state queried for %s", req.RoomID)
|
||||||
|
return fmt.Errorf("unknown room")
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *sendEventTestRoomserverAPI) QuerySenderIDForUser(
|
||||||
|
ctx context.Context,
|
||||||
|
roomID spec.RoomID,
|
||||||
|
userID spec.UserID,
|
||||||
|
) (*spec.SenderID, error) {
|
||||||
|
if roomID.String() == s.roomIDStr {
|
||||||
|
if s.roomVersion == gomatrixserverlib.RoomVersionPseudoIDs {
|
||||||
|
roomKey, ok := s.senderMapping[userID.String()]
|
||||||
|
if ok {
|
||||||
|
sender := spec.SenderIDFromPseudoIDKey(roomKey)
|
||||||
|
return &sender, nil
|
||||||
|
} else {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
senderID := spec.SenderIDFromUserID(userID)
|
||||||
|
return &senderID, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil, fmt.Errorf("room not found")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *sendEventTestRoomserverAPI) QueryUserIDForSender(
|
||||||
|
ctx context.Context,
|
||||||
|
roomID spec.RoomID,
|
||||||
|
senderID spec.SenderID,
|
||||||
|
) (*spec.UserID, error) {
|
||||||
|
if roomID.String() == s.roomIDStr {
|
||||||
|
if s.roomVersion == gomatrixserverlib.RoomVersionPseudoIDs {
|
||||||
|
for uID, roomKey := range s.senderMapping {
|
||||||
|
if string(spec.SenderIDFromPseudoIDKey(roomKey)) == string(senderID) {
|
||||||
|
parsedUserID, err := spec.NewUserID(uID, true)
|
||||||
|
if err != nil {
|
||||||
|
s.t.Fatalf("Mock QueryUserIDForSender failed: %s", err)
|
||||||
|
}
|
||||||
|
return parsedUserID, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
userID := senderID.ToUserID()
|
||||||
|
if userID == nil {
|
||||||
|
return nil, fmt.Errorf("bad sender ID")
|
||||||
|
}
|
||||||
|
return userID, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil, fmt.Errorf("room not found")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *sendEventTestRoomserverAPI) SigningIdentityFor(ctx context.Context, roomID spec.RoomID, sender spec.UserID) (fclient.SigningIdentity, error) {
|
||||||
|
if s.roomIDStr == roomID.String() {
|
||||||
|
if s.roomVersion == gomatrixserverlib.RoomVersionPseudoIDs {
|
||||||
|
roomKey, ok := s.senderMapping[sender.String()]
|
||||||
|
if !ok {
|
||||||
|
s.t.Logf("SigningIdentityFor used with unknown user ID: %v", sender.String())
|
||||||
|
return fclient.SigningIdentity{}, fmt.Errorf("could not get signing identity for %v", sender.String())
|
||||||
|
}
|
||||||
|
return fclient.SigningIdentity{PrivateKey: roomKey}, nil
|
||||||
|
} else {
|
||||||
|
return fclient.SigningIdentity{PrivateKey: ed25519.NewKeyFromSeed(make([]byte, 32))}, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return fclient.SigningIdentity{}, fmt.Errorf("room not found")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *sendEventTestRoomserverAPI) InputRoomEvents(ctx context.Context, req *rsapi.InputRoomEventsRequest, res *rsapi.InputRoomEventsResponse) {
|
||||||
|
s.savedInputRoomEvents = req.InputRoomEvents
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test that user ID state keys are translated correctly
|
||||||
|
func Test_SendEvent_PseudoIDStateKeys(t *testing.T) {
|
||||||
|
nonpseudoIDRoomVersion := gomatrixserverlib.RoomVersionV10
|
||||||
|
pseudoIDRoomVersion := gomatrixserverlib.RoomVersionPseudoIDs
|
||||||
|
|
||||||
|
senderKeySeed := make([]byte, 32)
|
||||||
|
senderUserID := "@testuser:domain"
|
||||||
|
senderPrivKey := ed25519.NewKeyFromSeed(senderKeySeed)
|
||||||
|
senderPseudoID := string(spec.SenderIDFromPseudoIDKey(senderPrivKey))
|
||||||
|
|
||||||
|
eventType := "com.example.test"
|
||||||
|
roomIDStr := "!id:domain"
|
||||||
|
|
||||||
|
device := &uapi.Device{
|
||||||
|
UserID: senderUserID,
|
||||||
|
}
|
||||||
|
|
||||||
|
t.Run("user ID state key are not translated to room key in non-pseudo ID room", func(t *testing.T) {
|
||||||
|
eventsJSON := []string{
|
||||||
|
fmt.Sprintf(`{"type":"m.room.create","state_key":"","room_id":"%v","sender":"%v","content":{"creator":"%v","room_version":"%v"}}`, roomIDStr, senderUserID, senderUserID, nonpseudoIDRoomVersion),
|
||||||
|
fmt.Sprintf(`{"type":"m.room.member","state_key":"%v","room_id":"%v","sender":"%v","content":{"membership":"join"}}`, senderUserID, roomIDStr, senderUserID),
|
||||||
|
}
|
||||||
|
|
||||||
|
roomState, err := createEvents(eventsJSON, nonpseudoIDRoomVersion)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("failed to prepare state events: %s", err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
rsAPI := &sendEventTestRoomserverAPI{
|
||||||
|
t: t,
|
||||||
|
roomIDStr: roomIDStr,
|
||||||
|
roomVersion: nonpseudoIDRoomVersion,
|
||||||
|
roomState: roomState,
|
||||||
|
}
|
||||||
|
|
||||||
|
req, err := http.NewRequest("POST", "https://domain", io.NopCloser(strings.NewReader("{}")))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("failed to make new request: %s", err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
cfg := &config.ClientAPI{}
|
||||||
|
|
||||||
|
resp := SendEvent(req, device, roomIDStr, eventType, nil, &senderUserID, cfg, rsAPI, nil)
|
||||||
|
|
||||||
|
if resp.Code != http.StatusOK {
|
||||||
|
t.Fatalf("non-200 HTTP code returned: %v\nfull response: %v", resp.Code, resp)
|
||||||
|
}
|
||||||
|
|
||||||
|
assert.Equal(t, len(rsAPI.savedInputRoomEvents), 1)
|
||||||
|
|
||||||
|
ev := rsAPI.savedInputRoomEvents[0]
|
||||||
|
stateKey := ev.Event.StateKey()
|
||||||
|
if stateKey == nil {
|
||||||
|
t.Fatalf("submitted InputRoomEvent has nil state key, when it should be %v", senderUserID)
|
||||||
|
}
|
||||||
|
if *stateKey != senderUserID {
|
||||||
|
t.Fatalf("expected submitted InputRoomEvent to have user ID state key\nfound: %v\nexpected: %v", *stateKey, senderUserID)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("user ID state key are translated to room key in pseudo ID room", func(t *testing.T) {
|
||||||
|
eventsJSON := []string{
|
||||||
|
fmt.Sprintf(`{"type":"m.room.create","state_key":"","room_id":"%v","sender":"%v","content":{"creator":"%v","room_version":"%v"}}`, roomIDStr, senderPseudoID, senderPseudoID, pseudoIDRoomVersion),
|
||||||
|
fmt.Sprintf(`{"type":"m.room.member","state_key":"%v","room_id":"%v","sender":"%v","content":{"membership":"join"}}`, senderPseudoID, roomIDStr, senderPseudoID),
|
||||||
|
}
|
||||||
|
|
||||||
|
roomState, err := createEvents(eventsJSON, pseudoIDRoomVersion)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("failed to prepare state events: %s", err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
rsAPI := &sendEventTestRoomserverAPI{
|
||||||
|
t: t,
|
||||||
|
roomIDStr: roomIDStr,
|
||||||
|
roomVersion: pseudoIDRoomVersion,
|
||||||
|
senderMapping: map[string]ed25519.PrivateKey{
|
||||||
|
senderUserID: senderPrivKey,
|
||||||
|
},
|
||||||
|
roomState: roomState,
|
||||||
|
}
|
||||||
|
|
||||||
|
req, err := http.NewRequest("POST", "https://domain", io.NopCloser(strings.NewReader("{}")))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("failed to make new request: %s", err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
cfg := &config.ClientAPI{}
|
||||||
|
|
||||||
|
resp := SendEvent(req, device, roomIDStr, eventType, nil, &senderUserID, cfg, rsAPI, nil)
|
||||||
|
|
||||||
|
if resp.Code != http.StatusOK {
|
||||||
|
t.Fatalf("non-200 HTTP code returned: %v\nfull response: %v", resp.Code, resp)
|
||||||
|
}
|
||||||
|
|
||||||
|
assert.Equal(t, len(rsAPI.savedInputRoomEvents), 1)
|
||||||
|
|
||||||
|
ev := rsAPI.savedInputRoomEvents[0]
|
||||||
|
stateKey := ev.Event.StateKey()
|
||||||
|
if stateKey == nil {
|
||||||
|
t.Fatalf("submitted InputRoomEvent has nil state key, when it should be %v", senderPseudoID)
|
||||||
|
}
|
||||||
|
if *stateKey != senderPseudoID {
|
||||||
|
t.Fatalf("expected submitted InputRoomEvent to have pseudo ID state key\nfound: %v\nexpected: %v", *stateKey, senderPseudoID)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func createEvents(eventsJSON []string, roomVer gomatrixserverlib.RoomVersion) ([]*types.HeaderedEvent, error) {
|
||||||
|
events := make([]*types.HeaderedEvent, len(eventsJSON))
|
||||||
|
|
||||||
|
roomVerImpl, err := gomatrixserverlib.GetRoomVersion(roomVer)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("no roomver impl: %s", err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
for i, eventJSON := range eventsJSON {
|
||||||
|
pdu, evErr := roomVerImpl.NewEventFromTrustedJSON([]byte(eventJSON), false)
|
||||||
|
if evErr != nil {
|
||||||
|
return nil, fmt.Errorf("failed to make event: %s", err.Error())
|
||||||
|
}
|
||||||
|
ev := types.HeaderedEvent{PDU: pdu}
|
||||||
|
events[i] = &ev
|
||||||
|
}
|
||||||
|
|
||||||
|
return events, nil
|
||||||
|
}
|
|
@ -19,10 +19,10 @@ import (
|
||||||
"github.com/matrix-org/util"
|
"github.com/matrix-org/util"
|
||||||
|
|
||||||
"github.com/matrix-org/dendrite/clientapi/httputil"
|
"github.com/matrix-org/dendrite/clientapi/httputil"
|
||||||
"github.com/matrix-org/dendrite/clientapi/jsonerror"
|
|
||||||
"github.com/matrix-org/dendrite/clientapi/producers"
|
"github.com/matrix-org/dendrite/clientapi/producers"
|
||||||
"github.com/matrix-org/dendrite/internal/transactions"
|
"github.com/matrix-org/dendrite/internal/transactions"
|
||||||
userapi "github.com/matrix-org/dendrite/userapi/api"
|
userapi "github.com/matrix-org/dendrite/userapi/api"
|
||||||
|
"github.com/matrix-org/gomatrixserverlib/spec"
|
||||||
)
|
)
|
||||||
|
|
||||||
// SendToDevice handles PUT /_matrix/client/r0/sendToDevice/{eventType}/{txnId}
|
// SendToDevice handles PUT /_matrix/client/r0/sendToDevice/{eventType}/{txnId}
|
||||||
|
@ -53,7 +53,10 @@ func SendToDevice(
|
||||||
req.Context(), device.UserID, userID, deviceID, eventType, message,
|
req.Context(), device.UserID, userID, deviceID, eventType, message,
|
||||||
); err != nil {
|
); err != nil {
|
||||||
util.GetLogger(req.Context()).WithError(err).Error("eduProducer.SendToDevice failed")
|
util.GetLogger(req.Context()).WithError(err).Error("eduProducer.SendToDevice failed")
|
||||||
return jsonerror.InternalServerError()
|
return util.JSONResponse{
|
||||||
|
Code: http.StatusInternalServerError,
|
||||||
|
JSON: spec.InternalServerError{},
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -15,12 +15,13 @@ package routing
|
||||||
import (
|
import (
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/matrix-org/util"
|
||||||
|
|
||||||
"github.com/matrix-org/dendrite/clientapi/httputil"
|
"github.com/matrix-org/dendrite/clientapi/httputil"
|
||||||
"github.com/matrix-org/dendrite/clientapi/jsonerror"
|
|
||||||
"github.com/matrix-org/dendrite/clientapi/producers"
|
"github.com/matrix-org/dendrite/clientapi/producers"
|
||||||
roomserverAPI "github.com/matrix-org/dendrite/roomserver/api"
|
roomserverAPI "github.com/matrix-org/dendrite/roomserver/api"
|
||||||
userapi "github.com/matrix-org/dendrite/userapi/api"
|
userapi "github.com/matrix-org/dendrite/userapi/api"
|
||||||
"github.com/matrix-org/util"
|
"github.com/matrix-org/gomatrixserverlib/spec"
|
||||||
)
|
)
|
||||||
|
|
||||||
type typingContentJSON struct {
|
type typingContentJSON struct {
|
||||||
|
@ -38,12 +39,20 @@ func SendTyping(
|
||||||
if device.UserID != userID {
|
if device.UserID != userID {
|
||||||
return util.JSONResponse{
|
return util.JSONResponse{
|
||||||
Code: http.StatusForbidden,
|
Code: http.StatusForbidden,
|
||||||
JSON: jsonerror.Forbidden("Cannot set another user's typing state"),
|
JSON: spec.Forbidden("Cannot set another user's typing state"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
deviceUserID, err := spec.NewUserID(userID, true)
|
||||||
|
if err != nil {
|
||||||
|
return util.JSONResponse{
|
||||||
|
Code: http.StatusForbidden,
|
||||||
|
JSON: spec.Forbidden("userID doesn't have power level to change visibility"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Verify that the user is a member of this room
|
// Verify that the user is a member of this room
|
||||||
resErr := checkMemberInRoom(req.Context(), rsAPI, userID, roomID)
|
resErr := checkMemberInRoom(req.Context(), rsAPI, *deviceUserID, roomID)
|
||||||
if resErr != nil {
|
if resErr != nil {
|
||||||
return *resErr
|
return *resErr
|
||||||
}
|
}
|
||||||
|
@ -57,7 +66,10 @@ func SendTyping(
|
||||||
|
|
||||||
if err := syncProducer.SendTyping(req.Context(), userID, roomID, r.Typing, r.Timeout); err != nil {
|
if err := syncProducer.SendTyping(req.Context(), userID, roomID, r.Typing, r.Timeout); err != nil {
|
||||||
util.GetLogger(req.Context()).WithError(err).Error("eduProducer.Send failed")
|
util.GetLogger(req.Context()).WithError(err).Error("eduProducer.Send failed")
|
||||||
return jsonerror.InternalServerError()
|
return util.JSONResponse{
|
||||||
|
Code: http.StatusInternalServerError,
|
||||||
|
JSON: spec.InternalServerError{},
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return util.JSONResponse{
|
return util.JSONResponse{
|
||||||
|
|
|
@ -22,22 +22,21 @@ import (
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/matrix-org/gomatrix"
|
"github.com/matrix-org/gomatrix"
|
||||||
"github.com/matrix-org/gomatrixserverlib"
|
|
||||||
"github.com/matrix-org/gomatrixserverlib/tokens"
|
"github.com/matrix-org/gomatrixserverlib/tokens"
|
||||||
"github.com/matrix-org/util"
|
"github.com/matrix-org/util"
|
||||||
"github.com/prometheus/client_golang/prometheus"
|
"github.com/prometheus/client_golang/prometheus"
|
||||||
"github.com/sirupsen/logrus"
|
"github.com/sirupsen/logrus"
|
||||||
|
|
||||||
"github.com/matrix-org/dendrite/roomserver/version"
|
"github.com/matrix-org/dendrite/roomserver/types"
|
||||||
|
|
||||||
appserviceAPI "github.com/matrix-org/dendrite/appservice/api"
|
appserviceAPI "github.com/matrix-org/dendrite/appservice/api"
|
||||||
"github.com/matrix-org/dendrite/clientapi/httputil"
|
"github.com/matrix-org/dendrite/clientapi/httputil"
|
||||||
"github.com/matrix-org/dendrite/clientapi/jsonerror"
|
|
||||||
"github.com/matrix-org/dendrite/internal/eventutil"
|
"github.com/matrix-org/dendrite/internal/eventutil"
|
||||||
"github.com/matrix-org/dendrite/internal/transactions"
|
"github.com/matrix-org/dendrite/internal/transactions"
|
||||||
"github.com/matrix-org/dendrite/roomserver/api"
|
"github.com/matrix-org/dendrite/roomserver/api"
|
||||||
"github.com/matrix-org/dendrite/setup/config"
|
"github.com/matrix-org/dendrite/setup/config"
|
||||||
userapi "github.com/matrix-org/dendrite/userapi/api"
|
userapi "github.com/matrix-org/dendrite/userapi/api"
|
||||||
|
"github.com/matrix-org/gomatrixserverlib/spec"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Unspecced server notice request
|
// Unspecced server notice request
|
||||||
|
@ -52,6 +51,7 @@ type sendServerNoticeRequest struct {
|
||||||
StateKey string `json:"state_key,omitempty"`
|
StateKey string `json:"state_key,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// nolint:gocyclo
|
||||||
// SendServerNotice sends a message to a specific user. It can only be invoked by an admin.
|
// SendServerNotice sends a message to a specific user. It can only be invoked by an admin.
|
||||||
func SendServerNotice(
|
func SendServerNotice(
|
||||||
req *http.Request,
|
req *http.Request,
|
||||||
|
@ -68,7 +68,7 @@ func SendServerNotice(
|
||||||
if device.AccountType != userapi.AccountTypeAdmin {
|
if device.AccountType != userapi.AccountTypeAdmin {
|
||||||
return util.JSONResponse{
|
return util.JSONResponse{
|
||||||
Code: http.StatusForbidden,
|
Code: http.StatusForbidden,
|
||||||
JSON: jsonerror.Forbidden("This API can only be used by admin users."),
|
JSON: spec.Forbidden("This API can only be used by admin users."),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -90,38 +90,46 @@ func SendServerNotice(
|
||||||
if !r.valid() {
|
if !r.valid() {
|
||||||
return util.JSONResponse{
|
return util.JSONResponse{
|
||||||
Code: http.StatusBadRequest,
|
Code: http.StatusBadRequest,
|
||||||
JSON: jsonerror.BadJSON("Invalid request"),
|
JSON: spec.BadJSON("Invalid request"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
userID, err := spec.NewUserID(r.UserID, true)
|
||||||
|
if err != nil {
|
||||||
|
return util.JSONResponse{
|
||||||
|
Code: http.StatusBadRequest,
|
||||||
|
JSON: spec.InvalidParam("invalid user ID"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// get rooms for specified user
|
// get rooms for specified user
|
||||||
allUserRooms := []string{}
|
allUserRooms := []spec.RoomID{}
|
||||||
userRooms := api.QueryRoomsForUserResponse{}
|
|
||||||
// Get rooms the user is either joined, invited or has left.
|
// Get rooms the user is either joined, invited or has left.
|
||||||
for _, membership := range []string{"join", "invite", "leave"} {
|
for _, membership := range []string{"join", "invite", "leave"} {
|
||||||
if err := rsAPI.QueryRoomsForUser(ctx, &api.QueryRoomsForUserRequest{
|
userRooms, queryErr := rsAPI.QueryRoomsForUser(ctx, *userID, membership)
|
||||||
UserID: r.UserID,
|
if queryErr != nil {
|
||||||
WantMembership: membership,
|
|
||||||
}, &userRooms); err != nil {
|
|
||||||
return util.ErrorResponse(err)
|
return util.ErrorResponse(err)
|
||||||
}
|
}
|
||||||
allUserRooms = append(allUserRooms, userRooms.RoomIDs...)
|
allUserRooms = append(allUserRooms, userRooms...)
|
||||||
}
|
}
|
||||||
|
|
||||||
// get rooms of the sender
|
// get rooms of the sender
|
||||||
senderUserID := fmt.Sprintf("@%s:%s", cfgNotices.LocalPart, cfgClient.Matrix.ServerName)
|
senderUserID, err := spec.NewUserID(fmt.Sprintf("@%s:%s", cfgNotices.LocalPart, cfgClient.Matrix.ServerName), true)
|
||||||
senderRooms := api.QueryRoomsForUserResponse{}
|
if err != nil {
|
||||||
if err := rsAPI.QueryRoomsForUser(ctx, &api.QueryRoomsForUserRequest{
|
return util.JSONResponse{
|
||||||
UserID: senderUserID,
|
Code: http.StatusInternalServerError,
|
||||||
WantMembership: "join",
|
JSON: spec.Unknown("internal server error"),
|
||||||
}, &senderRooms); err != nil {
|
}
|
||||||
|
}
|
||||||
|
senderRooms, err := rsAPI.QueryRoomsForUser(ctx, *senderUserID, "join")
|
||||||
|
if err != nil {
|
||||||
return util.ErrorResponse(err)
|
return util.ErrorResponse(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// check if we have rooms in common
|
// check if we have rooms in common
|
||||||
commonRooms := []string{}
|
commonRooms := []spec.RoomID{}
|
||||||
for _, userRoomID := range allUserRooms {
|
for _, userRoomID := range allUserRooms {
|
||||||
for _, senderRoomID := range senderRooms.RoomIDs {
|
for _, senderRoomID := range senderRooms {
|
||||||
if userRoomID == senderRoomID {
|
if userRoomID == senderRoomID {
|
||||||
commonRooms = append(commonRooms, senderRoomID)
|
commonRooms = append(commonRooms, senderRoomID)
|
||||||
}
|
}
|
||||||
|
@ -134,12 +142,12 @@ func SendServerNotice(
|
||||||
|
|
||||||
var (
|
var (
|
||||||
roomID string
|
roomID string
|
||||||
roomVersion = version.DefaultRoomVersion()
|
roomVersion = rsAPI.DefaultRoomVersion()
|
||||||
)
|
)
|
||||||
|
|
||||||
// create a new room for the user
|
// create a new room for the user
|
||||||
if len(commonRooms) == 0 {
|
if len(commonRooms) == 0 {
|
||||||
powerLevelContent := eventutil.InitialPowerLevelsContent(senderUserID)
|
powerLevelContent := eventutil.InitialPowerLevelsContent(senderUserID.String())
|
||||||
powerLevelContent.Users[r.UserID] = -10 // taken from Synapse
|
powerLevelContent.Users[r.UserID] = -10 // taken from Synapse
|
||||||
pl, err := json.Marshal(powerLevelContent)
|
pl, err := json.Marshal(powerLevelContent)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -155,9 +163,8 @@ func SendServerNotice(
|
||||||
Invite: []string{r.UserID},
|
Invite: []string{r.UserID},
|
||||||
Name: cfgNotices.RoomName,
|
Name: cfgNotices.RoomName,
|
||||||
Visibility: "private",
|
Visibility: "private",
|
||||||
Preset: presetPrivateChat,
|
Preset: spec.PresetPrivateChat,
|
||||||
CreationContent: cc,
|
CreationContent: cc,
|
||||||
GuestCanJoin: false,
|
|
||||||
RoomVersion: roomVersion,
|
RoomVersion: roomVersion,
|
||||||
PowerLevelContentOverride: pl,
|
PowerLevelContentOverride: pl,
|
||||||
}
|
}
|
||||||
|
@ -176,7 +183,10 @@ func SendServerNotice(
|
||||||
}}
|
}}
|
||||||
if err = saveTagData(req, r.UserID, roomID, userAPI, serverAlertTag); err != nil {
|
if err = saveTagData(req, r.UserID, roomID, userAPI, serverAlertTag); err != nil {
|
||||||
util.GetLogger(ctx).WithError(err).Error("saveTagData failed")
|
util.GetLogger(ctx).WithError(err).Error("saveTagData failed")
|
||||||
return jsonerror.InternalServerError()
|
return util.JSONResponse{
|
||||||
|
Code: http.StatusInternalServerError,
|
||||||
|
JSON: spec.InternalServerError{},
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
default:
|
default:
|
||||||
|
@ -185,16 +195,27 @@ func SendServerNotice(
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// we've found a room in common, check the membership
|
// we've found a room in common, check the membership
|
||||||
roomID = commonRooms[0]
|
deviceUserID, err := spec.NewUserID(r.UserID, true)
|
||||||
|
if err != nil {
|
||||||
|
return util.JSONResponse{
|
||||||
|
Code: http.StatusForbidden,
|
||||||
|
JSON: spec.Forbidden("userID doesn't have power level to change visibility"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
roomID = commonRooms[0].String()
|
||||||
membershipRes := api.QueryMembershipForUserResponse{}
|
membershipRes := api.QueryMembershipForUserResponse{}
|
||||||
err := rsAPI.QueryMembershipForUser(ctx, &api.QueryMembershipForUserRequest{UserID: r.UserID, RoomID: roomID}, &membershipRes)
|
err = rsAPI.QueryMembershipForUser(ctx, &api.QueryMembershipForUserRequest{UserID: *deviceUserID, RoomID: roomID}, &membershipRes)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
util.GetLogger(ctx).WithError(err).Error("unable to query membership for user")
|
util.GetLogger(ctx).WithError(err).Error("unable to query membership for user")
|
||||||
return jsonerror.InternalServerError()
|
return util.JSONResponse{
|
||||||
|
Code: http.StatusInternalServerError,
|
||||||
|
JSON: spec.InternalServerError{},
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if !membershipRes.IsInRoom {
|
if !membershipRes.IsInRoom {
|
||||||
// re-invite the user
|
// re-invite the user
|
||||||
res, err := sendInvite(ctx, userAPI, senderDevice, roomID, r.UserID, "Server notice room", cfgClient, rsAPI, asAPI, time.Now())
|
res, err := sendInvite(ctx, senderDevice, roomID, r.UserID, "Server notice room", cfgClient, rsAPI, time.Now())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return res
|
return res
|
||||||
}
|
}
|
||||||
|
@ -207,7 +228,7 @@ func SendServerNotice(
|
||||||
"body": r.Content.Body,
|
"body": r.Content.Body,
|
||||||
"msgtype": r.Content.MsgType,
|
"msgtype": r.Content.MsgType,
|
||||||
}
|
}
|
||||||
e, resErr := generateSendEvent(ctx, request, senderDevice, roomID, "m.room.message", nil, cfgClient, rsAPI, time.Now())
|
e, resErr := generateSendEvent(ctx, request, senderDevice, roomID, "m.room.message", nil, rsAPI, time.Now())
|
||||||
if resErr != nil {
|
if resErr != nil {
|
||||||
logrus.Errorf("failed to send message: %+v", resErr)
|
logrus.Errorf("failed to send message: %+v", resErr)
|
||||||
return *resErr
|
return *resErr
|
||||||
|
@ -228,8 +249,8 @@ func SendServerNotice(
|
||||||
if err := api.SendEvents(
|
if err := api.SendEvents(
|
||||||
ctx, rsAPI,
|
ctx, rsAPI,
|
||||||
api.KindNew,
|
api.KindNew,
|
||||||
[]*gomatrixserverlib.HeaderedEvent{
|
[]*types.HeaderedEvent{
|
||||||
e.Headered(roomVersion),
|
{PDU: e},
|
||||||
},
|
},
|
||||||
device.UserDomain(),
|
device.UserDomain(),
|
||||||
cfgClient.Matrix.ServerName,
|
cfgClient.Matrix.ServerName,
|
||||||
|
@ -238,7 +259,10 @@ func SendServerNotice(
|
||||||
false,
|
false,
|
||||||
); err != nil {
|
); err != nil {
|
||||||
util.GetLogger(ctx).WithError(err).Error("SendEvents failed")
|
util.GetLogger(ctx).WithError(err).Error("SendEvents failed")
|
||||||
return jsonerror.InternalServerError()
|
return util.JSONResponse{
|
||||||
|
Code: http.StatusInternalServerError,
|
||||||
|
JSON: spec.InternalServerError{},
|
||||||
|
}
|
||||||
}
|
}
|
||||||
util.GetLogger(ctx).WithFields(logrus.Fields{
|
util.GetLogger(ctx).WithFields(logrus.Fields{
|
||||||
"event_id": e.EventID(),
|
"event_id": e.EventID(),
|
||||||
|
@ -295,30 +319,28 @@ func getSenderDevice(
|
||||||
}
|
}
|
||||||
|
|
||||||
// Set the avatarurl for the user
|
// Set the avatarurl for the user
|
||||||
avatarRes := &userapi.PerformSetAvatarURLResponse{}
|
profile, avatarChanged, err := userAPI.SetAvatarURL(ctx,
|
||||||
if err = userAPI.SetAvatarURL(ctx, &userapi.PerformSetAvatarURLRequest{
|
cfg.Matrix.ServerNotices.LocalPart,
|
||||||
Localpart: cfg.Matrix.ServerNotices.LocalPart,
|
cfg.Matrix.ServerName,
|
||||||
ServerName: cfg.Matrix.ServerName,
|
cfg.Matrix.ServerNotices.AvatarURL,
|
||||||
AvatarURL: cfg.Matrix.ServerNotices.AvatarURL,
|
)
|
||||||
}, avatarRes); err != nil {
|
if err != nil {
|
||||||
util.GetLogger(ctx).WithError(err).Error("userAPI.SetAvatarURL failed")
|
util.GetLogger(ctx).WithError(err).Error("userAPI.SetAvatarURL failed")
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
profile := avatarRes.Profile
|
|
||||||
|
|
||||||
// Set the displayname for the user
|
// Set the displayname for the user
|
||||||
displayNameRes := &userapi.PerformUpdateDisplayNameResponse{}
|
_, displayNameChanged, err := userAPI.SetDisplayName(ctx,
|
||||||
if err = userAPI.SetDisplayName(ctx, &userapi.PerformUpdateDisplayNameRequest{
|
cfg.Matrix.ServerNotices.LocalPart,
|
||||||
Localpart: cfg.Matrix.ServerNotices.LocalPart,
|
cfg.Matrix.ServerName,
|
||||||
ServerName: cfg.Matrix.ServerName,
|
cfg.Matrix.ServerNotices.DisplayName,
|
||||||
DisplayName: cfg.Matrix.ServerNotices.DisplayName,
|
)
|
||||||
}, displayNameRes); err != nil {
|
if err != nil {
|
||||||
util.GetLogger(ctx).WithError(err).Error("userAPI.SetDisplayName failed")
|
util.GetLogger(ctx).WithError(err).Error("userAPI.SetDisplayName failed")
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
if displayNameRes.Changed {
|
if displayNameChanged {
|
||||||
profile.DisplayName = cfg.Matrix.ServerNotices.DisplayName
|
profile.DisplayName = cfg.Matrix.ServerNotices.DisplayName
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -334,8 +356,8 @@ func getSenderDevice(
|
||||||
// We've got an existing account, return the first device of it
|
// We've got an existing account, return the first device of it
|
||||||
if len(deviceRes.Devices) > 0 {
|
if len(deviceRes.Devices) > 0 {
|
||||||
// If there were changes to the profile, create a new membership event
|
// If there were changes to the profile, create a new membership event
|
||||||
if displayNameRes.Changed || avatarRes.Changed {
|
if displayNameChanged || avatarChanged {
|
||||||
_, err = updateProfile(ctx, rsAPI, &deviceRes.Devices[0], profile, accRes.Account.UserID, cfg, time.Now())
|
_, err = updateProfile(ctx, rsAPI, &deviceRes.Devices[0], profile, accRes.Account.UserID, time.Now())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
|
@ -20,16 +20,18 @@ import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
"github.com/matrix-org/dendrite/clientapi/jsonerror"
|
|
||||||
"github.com/matrix-org/dendrite/roomserver/api"
|
"github.com/matrix-org/dendrite/roomserver/api"
|
||||||
|
"github.com/matrix-org/dendrite/roomserver/types"
|
||||||
|
"github.com/matrix-org/dendrite/syncapi/synctypes"
|
||||||
userapi "github.com/matrix-org/dendrite/userapi/api"
|
userapi "github.com/matrix-org/dendrite/userapi/api"
|
||||||
"github.com/matrix-org/gomatrixserverlib"
|
"github.com/matrix-org/gomatrixserverlib"
|
||||||
|
"github.com/matrix-org/gomatrixserverlib/spec"
|
||||||
"github.com/matrix-org/util"
|
"github.com/matrix-org/util"
|
||||||
log "github.com/sirupsen/logrus"
|
log "github.com/sirupsen/logrus"
|
||||||
)
|
)
|
||||||
|
|
||||||
type stateEventInStateResp struct {
|
type stateEventInStateResp struct {
|
||||||
gomatrixserverlib.ClientEvent
|
synctypes.ClientEvent
|
||||||
PrevContent json.RawMessage `json:"prev_content,omitempty"`
|
PrevContent json.RawMessage `json:"prev_content,omitempty"`
|
||||||
ReplacesState string `json:"replaces_state,omitempty"`
|
ReplacesState string `json:"replaces_state,omitempty"`
|
||||||
}
|
}
|
||||||
|
@ -54,12 +56,15 @@ func OnIncomingStateRequest(ctx context.Context, device *userapi.Device, rsAPI a
|
||||||
StateToFetch: []gomatrixserverlib.StateKeyTuple{},
|
StateToFetch: []gomatrixserverlib.StateKeyTuple{},
|
||||||
}, &stateRes); err != nil {
|
}, &stateRes); err != nil {
|
||||||
util.GetLogger(ctx).WithError(err).Error("queryAPI.QueryLatestEventsAndState failed")
|
util.GetLogger(ctx).WithError(err).Error("queryAPI.QueryLatestEventsAndState failed")
|
||||||
return jsonerror.InternalServerError()
|
return util.JSONResponse{
|
||||||
|
Code: http.StatusInternalServerError,
|
||||||
|
JSON: spec.InternalServerError{},
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if !stateRes.RoomExists {
|
if !stateRes.RoomExists {
|
||||||
return util.JSONResponse{
|
return util.JSONResponse{
|
||||||
Code: http.StatusForbidden,
|
Code: http.StatusForbidden,
|
||||||
JSON: jsonerror.Forbidden("room does not exist"),
|
JSON: spec.Forbidden("room does not exist"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -67,11 +72,14 @@ func OnIncomingStateRequest(ctx context.Context, device *userapi.Device, rsAPI a
|
||||||
// that marks the room as world-readable. If we don't then we assume that
|
// that marks the room as world-readable. If we don't then we assume that
|
||||||
// the room is not world-readable.
|
// the room is not world-readable.
|
||||||
for _, ev := range stateRes.StateEvents {
|
for _, ev := range stateRes.StateEvents {
|
||||||
if ev.Type() == gomatrixserverlib.MRoomHistoryVisibility {
|
if ev.Type() == spec.MRoomHistoryVisibility {
|
||||||
content := map[string]string{}
|
content := map[string]string{}
|
||||||
if err := json.Unmarshal(ev.Content(), &content); err != nil {
|
if err := json.Unmarshal(ev.Content(), &content); err != nil {
|
||||||
util.GetLogger(ctx).WithError(err).Error("json.Unmarshal for history visibility failed")
|
util.GetLogger(ctx).WithError(err).Error("json.Unmarshal for history visibility failed")
|
||||||
return jsonerror.InternalServerError()
|
return util.JSONResponse{
|
||||||
|
Code: http.StatusInternalServerError,
|
||||||
|
JSON: spec.InternalServerError{},
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if visibility, ok := content["history_visibility"]; ok {
|
if visibility, ok := content["history_visibility"]; ok {
|
||||||
worldReadable = visibility == "world_readable"
|
worldReadable = visibility == "world_readable"
|
||||||
|
@ -91,20 +99,31 @@ func OnIncomingStateRequest(ctx context.Context, device *userapi.Device, rsAPI a
|
||||||
if !worldReadable {
|
if !worldReadable {
|
||||||
// The room isn't world-readable so try to work out based on the
|
// The room isn't world-readable so try to work out based on the
|
||||||
// user's membership if we want the latest state or not.
|
// user's membership if we want the latest state or not.
|
||||||
err := rsAPI.QueryMembershipForUser(ctx, &api.QueryMembershipForUserRequest{
|
userID, err := spec.NewUserID(device.UserID, true)
|
||||||
|
if err != nil {
|
||||||
|
util.GetLogger(ctx).WithError(err).Error("UserID is invalid")
|
||||||
|
return util.JSONResponse{
|
||||||
|
Code: http.StatusBadRequest,
|
||||||
|
JSON: spec.Unknown("Device UserID is invalid"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
err = rsAPI.QueryMembershipForUser(ctx, &api.QueryMembershipForUserRequest{
|
||||||
RoomID: roomID,
|
RoomID: roomID,
|
||||||
UserID: device.UserID,
|
UserID: *userID,
|
||||||
}, &membershipRes)
|
}, &membershipRes)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
util.GetLogger(ctx).WithError(err).Error("Failed to QueryMembershipForUser")
|
util.GetLogger(ctx).WithError(err).Error("Failed to QueryMembershipForUser")
|
||||||
return jsonerror.InternalServerError()
|
return util.JSONResponse{
|
||||||
|
Code: http.StatusInternalServerError,
|
||||||
|
JSON: spec.InternalServerError{},
|
||||||
|
}
|
||||||
}
|
}
|
||||||
// If the user has never been in the room then stop at this point.
|
// If the user has never been in the room then stop at this point.
|
||||||
// We won't tell the user about a room they have never joined.
|
// We won't tell the user about a room they have never joined.
|
||||||
if !membershipRes.HasBeenInRoom {
|
if !membershipRes.HasBeenInRoom {
|
||||||
return util.JSONResponse{
|
return util.JSONResponse{
|
||||||
Code: http.StatusForbidden,
|
Code: http.StatusForbidden,
|
||||||
JSON: jsonerror.Forbidden(fmt.Sprintf("Unknown room %q or user %q has never joined this room", roomID, device.UserID)),
|
JSON: spec.Forbidden(fmt.Sprintf("Unknown room %q or user %q has never joined this room", roomID, device.UserID)),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Otherwise, if the user has been in the room, whether or not we
|
// Otherwise, if the user has been in the room, whether or not we
|
||||||
|
@ -122,7 +141,7 @@ func OnIncomingStateRequest(ctx context.Context, device *userapi.Device, rsAPI a
|
||||||
"state_at_event": !wantLatestState,
|
"state_at_event": !wantLatestState,
|
||||||
}).Info("Fetching all state")
|
}).Info("Fetching all state")
|
||||||
|
|
||||||
stateEvents := []gomatrixserverlib.ClientEvent{}
|
stateEvents := []synctypes.ClientEvent{}
|
||||||
if wantLatestState {
|
if wantLatestState {
|
||||||
// If we are happy to use the latest state, either because the user is
|
// If we are happy to use the latest state, either because the user is
|
||||||
// still in the room, or because the room is world-readable, then just
|
// still in the room, or because the room is world-readable, then just
|
||||||
|
@ -131,7 +150,9 @@ func OnIncomingStateRequest(ctx context.Context, device *userapi.Device, rsAPI a
|
||||||
for _, ev := range stateRes.StateEvents {
|
for _, ev := range stateRes.StateEvents {
|
||||||
stateEvents = append(
|
stateEvents = append(
|
||||||
stateEvents,
|
stateEvents,
|
||||||
gomatrixserverlib.HeaderedToClientEvent(ev, gomatrixserverlib.FormatAll),
|
synctypes.ToClientEventDefault(func(roomID spec.RoomID, senderID spec.SenderID) (*spec.UserID, error) {
|
||||||
|
return rsAPI.QueryUserIDForSender(ctx, roomID, senderID)
|
||||||
|
}, ev),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
@ -145,12 +166,22 @@ func OnIncomingStateRequest(ctx context.Context, device *userapi.Device, rsAPI a
|
||||||
}, &stateAfterRes)
|
}, &stateAfterRes)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
util.GetLogger(ctx).WithError(err).Error("Failed to QueryMembershipForUser")
|
util.GetLogger(ctx).WithError(err).Error("Failed to QueryMembershipForUser")
|
||||||
return jsonerror.InternalServerError()
|
return util.JSONResponse{
|
||||||
|
Code: http.StatusInternalServerError,
|
||||||
|
JSON: spec.InternalServerError{},
|
||||||
|
}
|
||||||
}
|
}
|
||||||
for _, ev := range stateAfterRes.StateEvents {
|
for _, ev := range stateAfterRes.StateEvents {
|
||||||
|
clientEvent, err := synctypes.ToClientEvent(ev, synctypes.FormatAll, func(roomID spec.RoomID, senderID spec.SenderID) (*spec.UserID, error) {
|
||||||
|
return rsAPI.QueryUserIDForSender(ctx, roomID, senderID)
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
util.GetLogger(ctx).WithError(err).Error("Failed converting to ClientEvent")
|
||||||
|
continue
|
||||||
|
}
|
||||||
stateEvents = append(
|
stateEvents = append(
|
||||||
stateEvents,
|
stateEvents,
|
||||||
gomatrixserverlib.HeaderedToClientEvent(ev, gomatrixserverlib.FormatAll),
|
*clientEvent,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -174,6 +205,37 @@ func OnIncomingStateTypeRequest(
|
||||||
var worldReadable bool
|
var worldReadable bool
|
||||||
var wantLatestState bool
|
var wantLatestState bool
|
||||||
|
|
||||||
|
roomVer, err := rsAPI.QueryRoomVersionForRoom(ctx, roomID)
|
||||||
|
if err != nil {
|
||||||
|
return util.JSONResponse{
|
||||||
|
Code: http.StatusForbidden,
|
||||||
|
JSON: spec.Forbidden(fmt.Sprintf("Unknown room %q or user %q has never joined this room", roomID, device.UserID)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Translate user ID state keys to room keys in pseudo ID rooms
|
||||||
|
if roomVer == gomatrixserverlib.RoomVersionPseudoIDs {
|
||||||
|
parsedRoomID, err := spec.NewRoomID(roomID)
|
||||||
|
if err != nil {
|
||||||
|
return util.JSONResponse{
|
||||||
|
Code: http.StatusNotFound,
|
||||||
|
JSON: spec.InvalidParam("invalid room ID"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
newStateKey, err := synctypes.FromClientStateKey(*parsedRoomID, stateKey, func(roomID spec.RoomID, userID spec.UserID) (*spec.SenderID, error) {
|
||||||
|
return rsAPI.QuerySenderIDForUser(ctx, roomID, userID)
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
// TODO: work out better logic for failure cases (e.g. sender ID not found)
|
||||||
|
util.GetLogger(ctx).WithError(err).Error("synctypes.FromClientStateKey failed")
|
||||||
|
return util.JSONResponse{
|
||||||
|
Code: http.StatusInternalServerError,
|
||||||
|
JSON: spec.Unknown("internal server error"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
stateKey = *newStateKey
|
||||||
|
}
|
||||||
|
|
||||||
// Always fetch visibility so that we can work out whether to show
|
// Always fetch visibility so that we can work out whether to show
|
||||||
// the latest events or the last event from when the user was joined.
|
// the latest events or the last event from when the user was joined.
|
||||||
// Then include the requested event type and state key, assuming it
|
// Then include the requested event type and state key, assuming it
|
||||||
|
@ -184,9 +246,9 @@ func OnIncomingStateTypeRequest(
|
||||||
StateKey: stateKey,
|
StateKey: stateKey,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
if evType != gomatrixserverlib.MRoomHistoryVisibility && stateKey != "" {
|
if evType != spec.MRoomHistoryVisibility && stateKey != "" {
|
||||||
stateToFetch = append(stateToFetch, gomatrixserverlib.StateKeyTuple{
|
stateToFetch = append(stateToFetch, gomatrixserverlib.StateKeyTuple{
|
||||||
EventType: gomatrixserverlib.MRoomHistoryVisibility,
|
EventType: spec.MRoomHistoryVisibility,
|
||||||
StateKey: "",
|
StateKey: "",
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@ -200,18 +262,24 @@ func OnIncomingStateTypeRequest(
|
||||||
StateToFetch: stateToFetch,
|
StateToFetch: stateToFetch,
|
||||||
}, &stateRes); err != nil {
|
}, &stateRes); err != nil {
|
||||||
util.GetLogger(ctx).WithError(err).Error("queryAPI.QueryLatestEventsAndState failed")
|
util.GetLogger(ctx).WithError(err).Error("queryAPI.QueryLatestEventsAndState failed")
|
||||||
return jsonerror.InternalServerError()
|
return util.JSONResponse{
|
||||||
|
Code: http.StatusInternalServerError,
|
||||||
|
JSON: spec.InternalServerError{},
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Look at the room state and see if we have a history visibility event
|
// Look at the room state and see if we have a history visibility event
|
||||||
// that marks the room as world-readable. If we don't then we assume that
|
// that marks the room as world-readable. If we don't then we assume that
|
||||||
// the room is not world-readable.
|
// the room is not world-readable.
|
||||||
for _, ev := range stateRes.StateEvents {
|
for _, ev := range stateRes.StateEvents {
|
||||||
if ev.Type() == gomatrixserverlib.MRoomHistoryVisibility {
|
if ev.Type() == spec.MRoomHistoryVisibility {
|
||||||
content := map[string]string{}
|
content := map[string]string{}
|
||||||
if err := json.Unmarshal(ev.Content(), &content); err != nil {
|
if err := json.Unmarshal(ev.Content(), &content); err != nil {
|
||||||
util.GetLogger(ctx).WithError(err).Error("json.Unmarshal for history visibility failed")
|
util.GetLogger(ctx).WithError(err).Error("json.Unmarshal for history visibility failed")
|
||||||
return jsonerror.InternalServerError()
|
return util.JSONResponse{
|
||||||
|
Code: http.StatusInternalServerError,
|
||||||
|
JSON: spec.InternalServerError{},
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if visibility, ok := content["history_visibility"]; ok {
|
if visibility, ok := content["history_visibility"]; ok {
|
||||||
worldReadable = visibility == "world_readable"
|
worldReadable = visibility == "world_readable"
|
||||||
|
@ -229,22 +297,33 @@ func OnIncomingStateTypeRequest(
|
||||||
// membershipRes will only be populated if the room is not world-readable.
|
// membershipRes will only be populated if the room is not world-readable.
|
||||||
var membershipRes api.QueryMembershipForUserResponse
|
var membershipRes api.QueryMembershipForUserResponse
|
||||||
if !worldReadable {
|
if !worldReadable {
|
||||||
|
userID, err := spec.NewUserID(device.UserID, true)
|
||||||
|
if err != nil {
|
||||||
|
util.GetLogger(ctx).WithError(err).Error("UserID is invalid")
|
||||||
|
return util.JSONResponse{
|
||||||
|
Code: http.StatusBadRequest,
|
||||||
|
JSON: spec.Unknown("Device UserID is invalid"),
|
||||||
|
}
|
||||||
|
}
|
||||||
// The room isn't world-readable so try to work out based on the
|
// The room isn't world-readable so try to work out based on the
|
||||||
// user's membership if we want the latest state or not.
|
// user's membership if we want the latest state or not.
|
||||||
err := rsAPI.QueryMembershipForUser(ctx, &api.QueryMembershipForUserRequest{
|
err = rsAPI.QueryMembershipForUser(ctx, &api.QueryMembershipForUserRequest{
|
||||||
RoomID: roomID,
|
RoomID: roomID,
|
||||||
UserID: device.UserID,
|
UserID: *userID,
|
||||||
}, &membershipRes)
|
}, &membershipRes)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
util.GetLogger(ctx).WithError(err).Error("Failed to QueryMembershipForUser")
|
util.GetLogger(ctx).WithError(err).Error("Failed to QueryMembershipForUser")
|
||||||
return jsonerror.InternalServerError()
|
return util.JSONResponse{
|
||||||
|
Code: http.StatusInternalServerError,
|
||||||
|
JSON: spec.InternalServerError{},
|
||||||
|
}
|
||||||
}
|
}
|
||||||
// If the user has never been in the room then stop at this point.
|
// If the user has never been in the room then stop at this point.
|
||||||
// We won't tell the user about a room they have never joined.
|
// We won't tell the user about a room they have never joined.
|
||||||
if !membershipRes.HasBeenInRoom || membershipRes.Membership == gomatrixserverlib.Ban {
|
if !membershipRes.HasBeenInRoom || membershipRes.Membership == spec.Ban {
|
||||||
return util.JSONResponse{
|
return util.JSONResponse{
|
||||||
Code: http.StatusForbidden,
|
Code: http.StatusForbidden,
|
||||||
JSON: jsonerror.Forbidden(fmt.Sprintf("Unknown room %q or user %q has never joined this room", roomID, device.UserID)),
|
JSON: spec.Forbidden(fmt.Sprintf("Unknown room %q or user %q has never joined this room", roomID, device.UserID)),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Otherwise, if the user has been in the room, whether or not we
|
// Otherwise, if the user has been in the room, whether or not we
|
||||||
|
@ -264,7 +343,7 @@ func OnIncomingStateTypeRequest(
|
||||||
"state_at_event": !wantLatestState,
|
"state_at_event": !wantLatestState,
|
||||||
}).Info("Fetching state")
|
}).Info("Fetching state")
|
||||||
|
|
||||||
var event *gomatrixserverlib.HeaderedEvent
|
var event *types.HeaderedEvent
|
||||||
if wantLatestState {
|
if wantLatestState {
|
||||||
// If we are happy to use the latest state, either because the user is
|
// If we are happy to use the latest state, either because the user is
|
||||||
// still in the room, or because the room is world-readable, then just
|
// still in the room, or because the room is world-readable, then just
|
||||||
|
@ -292,7 +371,10 @@ func OnIncomingStateTypeRequest(
|
||||||
}, &stateAfterRes)
|
}, &stateAfterRes)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
util.GetLogger(ctx).WithError(err).Error("Failed to QueryMembershipForUser")
|
util.GetLogger(ctx).WithError(err).Error("Failed to QueryMembershipForUser")
|
||||||
return jsonerror.InternalServerError()
|
return util.JSONResponse{
|
||||||
|
Code: http.StatusInternalServerError,
|
||||||
|
JSON: spec.InternalServerError{},
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if len(stateAfterRes.StateEvents) > 0 {
|
if len(stateAfterRes.StateEvents) > 0 {
|
||||||
event = stateAfterRes.StateEvents[0]
|
event = stateAfterRes.StateEvents[0]
|
||||||
|
@ -304,12 +386,14 @@ func OnIncomingStateTypeRequest(
|
||||||
if event == nil {
|
if event == nil {
|
||||||
return util.JSONResponse{
|
return util.JSONResponse{
|
||||||
Code: http.StatusNotFound,
|
Code: http.StatusNotFound,
|
||||||
JSON: jsonerror.NotFound(fmt.Sprintf("Cannot find state event for %q", evType)),
|
JSON: spec.NotFound(fmt.Sprintf("Cannot find state event for %q", evType)),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
stateEvent := stateEventInStateResp{
|
stateEvent := stateEventInStateResp{
|
||||||
ClientEvent: gomatrixserverlib.HeaderedToClientEvent(event, gomatrixserverlib.FormatAll),
|
ClientEvent: synctypes.ToClientEventDefault(func(roomID spec.RoomID, senderID spec.SenderID) (*spec.UserID, error) {
|
||||||
|
return rsAPI.QueryUserIDForSender(ctx, roomID, senderID)
|
||||||
|
}, event),
|
||||||
}
|
}
|
||||||
|
|
||||||
var res interface{}
|
var res interface{}
|
||||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue