mirror of
https://mau.dev/maunium/synapse.git
synced 2024-06-09 22:28:55 +02:00
Compare commits
188 commits
Author | SHA1 | Date | |
---|---|---|---|
896b1ea2ba | |||
e2f8476044 | |||
18c1196893 | |||
8a3270075b | |||
f458dff16d | |||
6b709c512d | |||
5c2a837e3c | |||
966a886b36 | |||
535e97da7b | |||
64f5a4a353 | |||
7dd14fadb1 | |||
5624c8b961 | |||
4e3868dc46 | |||
d16910ca02 | |||
225f378ffa | |||
8bd9ff0783 | |||
466f344547 | |||
726006cdf2 | |||
967b6948b0 | |||
d7198dfb95 | |||
94ef2f4f5d | |||
bb5a692946 | |||
ad179b0136 | |||
5147ce294a | |||
acfc71a2de | |||
f35bc08d39 | |||
f2616edb73 | |||
86a2a0258f | |||
0893ee9af8 | |||
887f773472 | |||
9edb725ebc | |||
c97251d5ba | |||
7e2412265d | |||
7ef00b7628 | |||
b71d277438 | |||
a547b49773 | |||
6a9a641fb8 | |||
b5facbac0f | |||
b250ca5df2 | |||
048b912ac1 | |||
e0d420fbd1 | |||
9956f35c6a | |||
d464ee3602 | |||
439a095edc | |||
5d040f2066 | |||
f33266232e | |||
d43042864a | |||
f4ce030608 | |||
8b43cc89fa | |||
52af16c561 | |||
38f03a09ff | |||
c856ae4724 | |||
fe07995e69 | |||
52a649580f | |||
28a948f04f | |||
7cb3f8a979 | |||
fd12003441 | |||
5e892671a7 | |||
d2d48cce85 | |||
2359c64dec | |||
68dca8076f | |||
284d85dee3 | |||
fee7ccd51a | |||
ebe77381b0 | |||
0b91ccce47 | |||
ecf4e0674c | |||
7d82987b27 | |||
bd8d8865fb | |||
caf528477e | |||
f0c72d8e87 | |||
03a342b049 | |||
aa6345cb3b | |||
2b438df9b3 | |||
038b9ec59a | |||
59ac541310 | |||
a2e6f43f11 | |||
6f07fc4e00 | |||
4cf4a8281b | |||
ef7e040e54 | |||
393429d692 | |||
34a8652366 | |||
414ddcd457 | |||
4d408cb4dd | |||
212f150208 | |||
4c6e78fa14 | |||
1b155362ca | |||
522a40c4de | |||
dcd03d3b15 | |||
438bc23560 | |||
cf30cfe5d1 | |||
1726b49457 | |||
792cfe7ba6 | |||
c3682ff668 | |||
3e6ee8ff88 | |||
7c9ac01eb5 | |||
3818597751 | |||
3aadf43122 | |||
5b6a75935e | |||
c0ea2bf800 | |||
37558d5e4c | |||
0b358f8643 | |||
7254015665 | |||
1e10f437cf | |||
e84a493f41 | |||
07232e27a8 | |||
e26673fe97 | |||
7ab0f630da | |||
b548f7803a | |||
758aec6b34 | |||
c897ac63e9 | |||
38bc7a009d | |||
6a275828c8 | |||
6e373468a4 | |||
48ee17dc79 | |||
f6437ca1c4 | |||
02bda250f8 | |||
0fd6b269d3 | |||
ef1db42843 | |||
89fc579329 | |||
9c91873922 | |||
41fbe387d6 | |||
90cc9e5b29 | |||
516fd891ee | |||
0ef2315a99 | |||
59710437e4 | |||
9985aa6821 | |||
31742149d4 | |||
947e8a6cb0 | |||
0d4d00a07c | |||
3166445514 | |||
922656fc77 | |||
30c50e0240 | |||
48a90c697b | |||
47773232b0 | |||
2e92b718d5 | |||
646cb6ff24 | |||
f85f2a0455 | |||
0fe9e1f7da | |||
ae181233aa | |||
074ef4d75f | |||
301c9771c4 | |||
800a5b6ef3 | |||
8c667759ad | |||
14e9ab19be | |||
20c8991a94 | |||
dcae2b4ba4 | |||
98f57ea3f2 | |||
f5b6005559 | |||
47f3870894 | |||
6d64f1b2b8 | |||
1d47532310 | |||
09f0957b36 | |||
803f05f60c | |||
c8e0bed426 | |||
28f5ad07d3 | |||
f0d6f14047 | |||
3a196b3227 | |||
4d5f585dee | |||
259442fa4c | |||
fe4719a268 | |||
3a30846bd0 | |||
15947bbd71 | |||
698ceabe2a | |||
67b2fad49e | |||
f2f54cb6af | |||
2ba175485f | |||
14c2066db6 | |||
15d050f5f4 | |||
aef880992a | |||
1cf18958a4 | |||
3a8e8c750c | |||
3568fb0874 | |||
9354d32fc9 | |||
0d0f138bbf | |||
0f5e09524d | |||
1b784b06d4 | |||
f4f711f28b | |||
de89885d15 | |||
3108b67232 | |||
b07561405c | |||
9eb9372eb4 | |||
ab635c80a7 | |||
5e7ff45534 | |||
0de822af4d | |||
83f9a6cdd5 | |||
78584d476c | |||
ce38046124 | |||
e95889bab3 |
|
@ -8,6 +8,7 @@
|
||||||
!README.rst
|
!README.rst
|
||||||
!pyproject.toml
|
!pyproject.toml
|
||||||
!poetry.lock
|
!poetry.lock
|
||||||
|
!requirements.txt
|
||||||
!Cargo.lock
|
!Cargo.lock
|
||||||
!Cargo.toml
|
!Cargo.toml
|
||||||
!build_rust.py
|
!build_rust.py
|
||||||
|
|
2
.github/workflows/docker.yml
vendored
2
.github/workflows/docker.yml
vendored
|
@ -30,7 +30,7 @@ jobs:
|
||||||
run: docker buildx inspect
|
run: docker buildx inspect
|
||||||
|
|
||||||
- name: Install Cosign
|
- name: Install Cosign
|
||||||
uses: sigstore/cosign-installer@v3.4.0
|
uses: sigstore/cosign-installer@v3.5.0
|
||||||
|
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
|
|
4
.github/workflows/docs-pr.yaml
vendored
4
.github/workflows/docs-pr.yaml
vendored
|
@ -19,7 +19,7 @@ jobs:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
|
|
||||||
- name: Setup mdbook
|
- name: Setup mdbook
|
||||||
uses: peaceiris/actions-mdbook@adeb05db28a0c0004681db83893d56c0388ea9ea # v1.2.0
|
uses: peaceiris/actions-mdbook@ee69d230fe19748b7abf22df32acaa93833fad08 # v2.0.0
|
||||||
with:
|
with:
|
||||||
mdbook-version: '0.4.17'
|
mdbook-version: '0.4.17'
|
||||||
|
|
||||||
|
@ -53,7 +53,7 @@ jobs:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
- name: Setup mdbook
|
- name: Setup mdbook
|
||||||
uses: peaceiris/actions-mdbook@adeb05db28a0c0004681db83893d56c0388ea9ea # v1.2.0
|
uses: peaceiris/actions-mdbook@ee69d230fe19748b7abf22df32acaa93833fad08 # v2.0.0
|
||||||
with:
|
with:
|
||||||
mdbook-version: '0.4.17'
|
mdbook-version: '0.4.17'
|
||||||
|
|
||||||
|
|
34
.github/workflows/docs.yaml
vendored
34
.github/workflows/docs.yaml
vendored
|
@ -56,7 +56,7 @@ jobs:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
|
|
||||||
- name: Setup mdbook
|
- name: Setup mdbook
|
||||||
uses: peaceiris/actions-mdbook@adeb05db28a0c0004681db83893d56c0388ea9ea # v1.2.0
|
uses: peaceiris/actions-mdbook@ee69d230fe19748b7abf22df32acaa93833fad08 # v2.0.0
|
||||||
with:
|
with:
|
||||||
mdbook-version: '0.4.17'
|
mdbook-version: '0.4.17'
|
||||||
|
|
||||||
|
@ -80,38 +80,8 @@ jobs:
|
||||||
|
|
||||||
# Deploy to the target directory.
|
# Deploy to the target directory.
|
||||||
- name: Deploy to gh pages
|
- name: Deploy to gh pages
|
||||||
uses: peaceiris/actions-gh-pages@373f7f263a76c20808c831209c920827a82a2847 # v3.9.3
|
uses: peaceiris/actions-gh-pages@4f9cc6602d3f66b9c108549d475ec49e8ef4d45e # v4.0.0
|
||||||
with:
|
with:
|
||||||
github_token: ${{ secrets.GITHUB_TOKEN }}
|
github_token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
publish_dir: ./book
|
publish_dir: ./book
|
||||||
destination_dir: ./${{ needs.pre.outputs.branch-version }}
|
destination_dir: ./${{ needs.pre.outputs.branch-version }}
|
||||||
|
|
||||||
################################################################################
|
|
||||||
pages-devdocs:
|
|
||||||
name: GitHub Pages (developer docs)
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
needs:
|
|
||||||
- pre
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v4
|
|
||||||
|
|
||||||
- name: "Set up Sphinx"
|
|
||||||
uses: matrix-org/setup-python-poetry@v1
|
|
||||||
with:
|
|
||||||
python-version: "3.x"
|
|
||||||
poetry-version: "1.3.2"
|
|
||||||
groups: "dev-docs"
|
|
||||||
extras: ""
|
|
||||||
|
|
||||||
- name: Build the documentation
|
|
||||||
run: |
|
|
||||||
cd dev-docs
|
|
||||||
poetry run make html
|
|
||||||
|
|
||||||
# Deploy to the target directory.
|
|
||||||
- name: Deploy to gh pages
|
|
||||||
uses: peaceiris/actions-gh-pages@373f7f263a76c20808c831209c920827a82a2847 # v3.9.3
|
|
||||||
with:
|
|
||||||
github_token: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
publish_dir: ./dev-docs/_build/html
|
|
||||||
destination_dir: ./dev-docs/${{ needs.pre.outputs.branch-version }}
|
|
||||||
|
|
18
.github/workflows/tests.yml
vendored
18
.github/workflows/tests.yml
vendored
|
@ -81,7 +81,7 @@ jobs:
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
- name: Install Rust
|
- name: Install Rust
|
||||||
uses: dtolnay/rust-toolchain@1.65.0
|
uses: dtolnay/rust-toolchain@1.66.0
|
||||||
- uses: Swatinem/rust-cache@v2
|
- uses: Swatinem/rust-cache@v2
|
||||||
- uses: matrix-org/setup-python-poetry@v1
|
- uses: matrix-org/setup-python-poetry@v1
|
||||||
with:
|
with:
|
||||||
|
@ -148,7 +148,7 @@ jobs:
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
- name: Install Rust
|
- name: Install Rust
|
||||||
uses: dtolnay/rust-toolchain@1.65.0
|
uses: dtolnay/rust-toolchain@1.66.0
|
||||||
- uses: Swatinem/rust-cache@v2
|
- uses: Swatinem/rust-cache@v2
|
||||||
|
|
||||||
- name: Setup Poetry
|
- name: Setup Poetry
|
||||||
|
@ -208,7 +208,7 @@ jobs:
|
||||||
with:
|
with:
|
||||||
ref: ${{ github.event.pull_request.head.sha }}
|
ref: ${{ github.event.pull_request.head.sha }}
|
||||||
- name: Install Rust
|
- name: Install Rust
|
||||||
uses: dtolnay/rust-toolchain@1.65.0
|
uses: dtolnay/rust-toolchain@1.66.0
|
||||||
- uses: Swatinem/rust-cache@v2
|
- uses: Swatinem/rust-cache@v2
|
||||||
- uses: matrix-org/setup-python-poetry@v1
|
- uses: matrix-org/setup-python-poetry@v1
|
||||||
with:
|
with:
|
||||||
|
@ -225,7 +225,7 @@ jobs:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
- name: Install Rust
|
- name: Install Rust
|
||||||
uses: dtolnay/rust-toolchain@1.65.0
|
uses: dtolnay/rust-toolchain@1.66.0
|
||||||
with:
|
with:
|
||||||
components: clippy
|
components: clippy
|
||||||
- uses: Swatinem/rust-cache@v2
|
- uses: Swatinem/rust-cache@v2
|
||||||
|
@ -344,7 +344,7 @@ jobs:
|
||||||
postgres:${{ matrix.job.postgres-version }}
|
postgres:${{ matrix.job.postgres-version }}
|
||||||
|
|
||||||
- name: Install Rust
|
- name: Install Rust
|
||||||
uses: dtolnay/rust-toolchain@1.65.0
|
uses: dtolnay/rust-toolchain@1.66.0
|
||||||
- uses: Swatinem/rust-cache@v2
|
- uses: Swatinem/rust-cache@v2
|
||||||
|
|
||||||
- uses: matrix-org/setup-python-poetry@v1
|
- uses: matrix-org/setup-python-poetry@v1
|
||||||
|
@ -386,7 +386,7 @@ jobs:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
- name: Install Rust
|
- name: Install Rust
|
||||||
uses: dtolnay/rust-toolchain@1.65.0
|
uses: dtolnay/rust-toolchain@1.66.0
|
||||||
- uses: Swatinem/rust-cache@v2
|
- uses: Swatinem/rust-cache@v2
|
||||||
|
|
||||||
# There aren't wheels for some of the older deps, so we need to install
|
# There aren't wheels for some of the older deps, so we need to install
|
||||||
|
@ -498,7 +498,7 @@ jobs:
|
||||||
run: cat sytest-blacklist .ci/worker-blacklist > synapse-blacklist-with-workers
|
run: cat sytest-blacklist .ci/worker-blacklist > synapse-blacklist-with-workers
|
||||||
|
|
||||||
- name: Install Rust
|
- name: Install Rust
|
||||||
uses: dtolnay/rust-toolchain@1.65.0
|
uses: dtolnay/rust-toolchain@1.66.0
|
||||||
- uses: Swatinem/rust-cache@v2
|
- uses: Swatinem/rust-cache@v2
|
||||||
|
|
||||||
- name: Run SyTest
|
- name: Run SyTest
|
||||||
|
@ -642,7 +642,7 @@ jobs:
|
||||||
path: synapse
|
path: synapse
|
||||||
|
|
||||||
- name: Install Rust
|
- name: Install Rust
|
||||||
uses: dtolnay/rust-toolchain@1.65.0
|
uses: dtolnay/rust-toolchain@1.66.0
|
||||||
- uses: Swatinem/rust-cache@v2
|
- uses: Swatinem/rust-cache@v2
|
||||||
|
|
||||||
- name: Prepare Complement's Prerequisites
|
- name: Prepare Complement's Prerequisites
|
||||||
|
@ -674,7 +674,7 @@ jobs:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
- name: Install Rust
|
- name: Install Rust
|
||||||
uses: dtolnay/rust-toolchain@1.65.0
|
uses: dtolnay/rust-toolchain@1.66.0
|
||||||
- uses: Swatinem/rust-cache@v2
|
- uses: Swatinem/rust-cache@v2
|
||||||
|
|
||||||
- run: cargo test
|
- run: cargo test
|
||||||
|
|
19
.gitlab-ci.yml
Normal file
19
.gitlab-ci.yml
Normal file
|
@ -0,0 +1,19 @@
|
||||||
|
image: docker:stable
|
||||||
|
|
||||||
|
stages:
|
||||||
|
- build
|
||||||
|
|
||||||
|
build amd64:
|
||||||
|
stage: build
|
||||||
|
tags:
|
||||||
|
- amd64
|
||||||
|
only:
|
||||||
|
- master
|
||||||
|
before_script:
|
||||||
|
- docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY
|
||||||
|
script:
|
||||||
|
- synversion=$(cat pyproject.toml | grep '^version =' | sed -E 's/^version = "(.+)"$/\1/')
|
||||||
|
- docker build --tag $CI_REGISTRY_IMAGE:latest --tag $CI_REGISTRY_IMAGE:$synversion .
|
||||||
|
- docker push $CI_REGISTRY_IMAGE:latest
|
||||||
|
- docker push $CI_REGISTRY_IMAGE:$synversion
|
||||||
|
- docker rmi $CI_REGISTRY_IMAGE:latest $CI_REGISTRY_IMAGE:$synversion
|
221
CHANGES.md
221
CHANGES.md
|
@ -1,3 +1,224 @@
|
||||||
|
# Synapse 1.109.0rc1 (2024-06-04)
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
- Add the ability to auto-accept invites on the behalf of users. See the [`auto_accept_invites`](https://element-hq.github.io/synapse/latest/usage/configuration/config_documentation.html#auto-accept-invites) config option for details. ([\#17147](https://github.com/element-hq/synapse/issues/17147))
|
||||||
|
- Add experimental [MSC3575](https://github.com/matrix-org/matrix-spec-proposals/pull/3575) Sliding Sync `/sync/e2ee` endpoint for to-device messages and device encryption info. ([\#17167](https://github.com/element-hq/synapse/issues/17167))
|
||||||
|
- Support [MSC3916](https://github.com/matrix-org/matrix-spec-proposals/issues/3916) by adding unstable media endpoints to `/_matrix/client`. ([\#17213](https://github.com/element-hq/synapse/issues/17213))
|
||||||
|
- Add logging to tasks managed by the task scheduler, showing CPU and database usage. ([\#17219](https://github.com/element-hq/synapse/issues/17219))
|
||||||
|
|
||||||
|
### Bugfixes
|
||||||
|
|
||||||
|
- Fix deduplicating of membership events to not create unused state groups. ([\#17164](https://github.com/element-hq/synapse/issues/17164))
|
||||||
|
- Fix bug where duplicate events could be sent down sync when using workers that are overloaded. ([\#17215](https://github.com/element-hq/synapse/issues/17215))
|
||||||
|
- Ignore attempts to send to-device messages to bad users, to avoid log spam when we try to connect to the bad server. ([\#17240](https://github.com/element-hq/synapse/issues/17240))
|
||||||
|
- Fix handling of duplicate concurrent uploading of device one-time-keys. ([\#17241](https://github.com/element-hq/synapse/issues/17241))
|
||||||
|
- Fix reporting of default tags to Sentry, such as worker name. Broke in v1.108.0. ([\#17251](https://github.com/element-hq/synapse/issues/17251))
|
||||||
|
- Fix bug where typing updates would not be sent when using workers after a restart. ([\#17252](https://github.com/element-hq/synapse/issues/17252))
|
||||||
|
|
||||||
|
### Improved Documentation
|
||||||
|
|
||||||
|
- Update the LemonLDAP documentation to say that claims should be explicitly included in the returned `id_token`, as Synapse won't request them. ([\#17204](https://github.com/element-hq/synapse/issues/17204))
|
||||||
|
|
||||||
|
### Internal Changes
|
||||||
|
|
||||||
|
- Improve DB usage when fetching related events. ([\#17083](https://github.com/element-hq/synapse/issues/17083))
|
||||||
|
- Log exceptions when failing to auto-join new user according to the `auto_join_rooms` option. ([\#17176](https://github.com/element-hq/synapse/issues/17176))
|
||||||
|
- Reduce work of calculating outbound device lists updates. ([\#17211](https://github.com/element-hq/synapse/issues/17211))
|
||||||
|
- Improve performance of calculating device lists changes in `/sync`. ([\#17216](https://github.com/element-hq/synapse/issues/17216))
|
||||||
|
- Move towards using `MultiWriterIdGenerator` everywhere. ([\#17226](https://github.com/element-hq/synapse/issues/17226))
|
||||||
|
- Replaces all usages of `StreamIdGenerator` with `MultiWriterIdGenerator`. ([\#17229](https://github.com/element-hq/synapse/issues/17229))
|
||||||
|
- Change the `allow_unsafe_locale` config option to also apply when setting up new databases. ([\#17238](https://github.com/element-hq/synapse/issues/17238))
|
||||||
|
- Fix errors in logs about closing incorrect logging contexts when media gets rejected by a module. ([\#17239](https://github.com/element-hq/synapse/issues/17239), [\#17246](https://github.com/element-hq/synapse/issues/17246))
|
||||||
|
- Clean out invalid destinations from `device_federation_outbox` table. ([\#17242](https://github.com/element-hq/synapse/issues/17242))
|
||||||
|
- Stop logging errors when receiving invalid User IDs in key querys requests. ([\#17250](https://github.com/element-hq/synapse/issues/17250))
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
### Updates to locked dependencies
|
||||||
|
|
||||||
|
* Bump anyhow from 1.0.83 to 1.0.86. ([\#17220](https://github.com/element-hq/synapse/issues/17220))
|
||||||
|
* Bump bcrypt from 4.1.2 to 4.1.3. ([\#17224](https://github.com/element-hq/synapse/issues/17224))
|
||||||
|
* Bump lxml from 5.2.1 to 5.2.2. ([\#17261](https://github.com/element-hq/synapse/issues/17261))
|
||||||
|
* Bump mypy-zope from 1.0.3 to 1.0.4. ([\#17262](https://github.com/element-hq/synapse/issues/17262))
|
||||||
|
* Bump phonenumbers from 8.13.35 to 8.13.37. ([\#17235](https://github.com/element-hq/synapse/issues/17235))
|
||||||
|
* Bump prometheus-client from 0.19.0 to 0.20.0. ([\#17233](https://github.com/element-hq/synapse/issues/17233))
|
||||||
|
* Bump pyasn1 from 0.5.1 to 0.6.0. ([\#17223](https://github.com/element-hq/synapse/issues/17223))
|
||||||
|
* Bump pyicu from 2.13 to 2.13.1. ([\#17236](https://github.com/element-hq/synapse/issues/17236))
|
||||||
|
* Bump pyopenssl from 24.0.0 to 24.1.0. ([\#17234](https://github.com/element-hq/synapse/issues/17234))
|
||||||
|
* Bump serde from 1.0.201 to 1.0.202. ([\#17221](https://github.com/element-hq/synapse/issues/17221))
|
||||||
|
* Bump serde from 1.0.202 to 1.0.203. ([\#17232](https://github.com/element-hq/synapse/issues/17232))
|
||||||
|
* Bump twine from 5.0.0 to 5.1.0. ([\#17225](https://github.com/element-hq/synapse/issues/17225))
|
||||||
|
* Bump types-psycopg2 from 2.9.21.20240311 to 2.9.21.20240417. ([\#17222](https://github.com/element-hq/synapse/issues/17222))
|
||||||
|
* Bump types-pyopenssl from 24.0.0.20240311 to 24.1.0.20240425. ([\#17260](https://github.com/element-hq/synapse/issues/17260))
|
||||||
|
|
||||||
|
# Synapse 1.108.0 (2024-05-28)
|
||||||
|
|
||||||
|
No significant changes since 1.108.0rc1.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
# Synapse 1.108.0rc1 (2024-05-21)
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
- Add a feature that allows clients to query the configured federation whitelist. Disabled by default. ([\#16848](https://github.com/element-hq/synapse/issues/16848), [\#17199](https://github.com/element-hq/synapse/issues/17199))
|
||||||
|
- Add the ability to allow numeric user IDs with a specific prefix when in the CAS flow. Contributed by Aurélien Grimpard. ([\#17098](https://github.com/element-hq/synapse/issues/17098))
|
||||||
|
|
||||||
|
### Bugfixes
|
||||||
|
|
||||||
|
- Fix bug where push rules would be empty in `/sync` for some accounts. Introduced in v1.93.0. ([\#17142](https://github.com/element-hq/synapse/issues/17142))
|
||||||
|
- Add support for optional whitespace around the Federation API's `Authorization` header's parameter commas. ([\#17145](https://github.com/element-hq/synapse/issues/17145))
|
||||||
|
- Fix bug where disabling room publication prevented public rooms being created on workers. ([\#17177](https://github.com/element-hq/synapse/issues/17177), [\#17184](https://github.com/element-hq/synapse/issues/17184))
|
||||||
|
|
||||||
|
### Improved Documentation
|
||||||
|
|
||||||
|
- Document [`/v1/make_knock`](https://spec.matrix.org/v1.10/server-server-api/#get_matrixfederationv1make_knockroomiduserid) and [`/v1/send_knock/`](https://spec.matrix.org/v1.10/server-server-api/#put_matrixfederationv1send_knockroomideventid) federation endpoints as worker-compatible. ([\#17058](https://github.com/element-hq/synapse/issues/17058))
|
||||||
|
- Update User Admin API with note about prefixing OIDC external_id providers. ([\#17139](https://github.com/element-hq/synapse/issues/17139))
|
||||||
|
- Clarify the state of the created room when using the `autocreate_auto_join_room_preset` config option. ([\#17150](https://github.com/element-hq/synapse/issues/17150))
|
||||||
|
- Update the Admin FAQ with the current libjemalloc version for latest Debian stable. Additionally update the name of the "push_rules" stream in the Workers documentation. ([\#17171](https://github.com/element-hq/synapse/issues/17171))
|
||||||
|
|
||||||
|
### Internal Changes
|
||||||
|
|
||||||
|
- Add note to reflect that [MSC3886](https://github.com/matrix-org/matrix-spec-proposals/pull/3886) is closed but will remain supported for some time. ([\#17151](https://github.com/element-hq/synapse/issues/17151))
|
||||||
|
- Update dependency PyO3 to 0.21. ([\#17162](https://github.com/element-hq/synapse/issues/17162))
|
||||||
|
- Fixes linter errors found in PR #17147. ([\#17166](https://github.com/element-hq/synapse/issues/17166))
|
||||||
|
- Bump black from 24.2.0 to 24.4.2. ([\#17170](https://github.com/element-hq/synapse/issues/17170))
|
||||||
|
- Cache literal sync filter validation for performance. ([\#17186](https://github.com/element-hq/synapse/issues/17186))
|
||||||
|
- Improve performance by fixing a reactor pause. ([\#17192](https://github.com/element-hq/synapse/issues/17192))
|
||||||
|
- Route `/make_knock` and `/send_knock` federation APIs to the federation reader worker in Complement test runs. ([\#17195](https://github.com/element-hq/synapse/issues/17195))
|
||||||
|
- Prepare sync handler to be able to return different sync responses (`SyncVersion`). ([\#17200](https://github.com/element-hq/synapse/issues/17200))
|
||||||
|
- Organize the sync cache key parameter outside of the sync config (separate concerns). ([\#17201](https://github.com/element-hq/synapse/issues/17201))
|
||||||
|
- Refactor `SyncResultBuilder` assembly to its own function. ([\#17202](https://github.com/element-hq/synapse/issues/17202))
|
||||||
|
- Rename to be obvious: `joined_rooms` -> `joined_room_ids`. ([\#17203](https://github.com/element-hq/synapse/issues/17203), [\#17208](https://github.com/element-hq/synapse/issues/17208))
|
||||||
|
- Add a short pause when rate-limiting a request. ([\#17210](https://github.com/element-hq/synapse/issues/17210))
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
### Updates to locked dependencies
|
||||||
|
|
||||||
|
* Bump cryptography from 42.0.5 to 42.0.7. ([\#17180](https://github.com/element-hq/synapse/issues/17180))
|
||||||
|
* Bump gitpython from 3.1.41 to 3.1.43. ([\#17181](https://github.com/element-hq/synapse/issues/17181))
|
||||||
|
* Bump immutabledict from 4.1.0 to 4.2.0. ([\#17179](https://github.com/element-hq/synapse/issues/17179))
|
||||||
|
* Bump sentry-sdk from 1.40.3 to 2.1.1. ([\#17178](https://github.com/element-hq/synapse/issues/17178))
|
||||||
|
* Bump serde from 1.0.200 to 1.0.201. ([\#17183](https://github.com/element-hq/synapse/issues/17183))
|
||||||
|
* Bump serde_json from 1.0.116 to 1.0.117. ([\#17182](https://github.com/element-hq/synapse/issues/17182))
|
||||||
|
|
||||||
|
Synapse 1.107.0 (2024-05-14)
|
||||||
|
============================
|
||||||
|
|
||||||
|
No significant changes since 1.107.0rc1.
|
||||||
|
|
||||||
|
|
||||||
|
# Synapse 1.107.0rc1 (2024-05-07)
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
- Add preliminary support for [MSC3823: Account Suspension](https://github.com/matrix-org/matrix-spec-proposals/pull/3823). ([\#17051](https://github.com/element-hq/synapse/issues/17051))
|
||||||
|
- Declare support for [Matrix v1.10](https://matrix.org/blog/2024/03/22/matrix-v1.10-release/). Contributed by @clokep. ([\#17082](https://github.com/element-hq/synapse/issues/17082))
|
||||||
|
- Add support for [MSC4115: membership metadata on events](https://github.com/matrix-org/matrix-spec-proposals/pull/4115). ([\#17104](https://github.com/element-hq/synapse/issues/17104), [\#17137](https://github.com/element-hq/synapse/issues/17137))
|
||||||
|
|
||||||
|
### Bugfixes
|
||||||
|
|
||||||
|
- Fixed search feature of Element Android on homesevers using SQLite by returning search terms as search highlights. ([\#17000](https://github.com/element-hq/synapse/issues/17000))
|
||||||
|
- Fixes a bug introduced in v1.52.0 where the `destination` query parameter for the [Destination Rooms Admin API](https://element-hq.github.io/synapse/v1.105/usage/administration/admin_api/federation.html#destination-rooms) failed to actually filter returned rooms. ([\#17077](https://github.com/element-hq/synapse/issues/17077))
|
||||||
|
- For MSC3266 room summaries, support queries at the recommended endpoint of `/_matrix/client/unstable/im.nheko.summary/summary/{roomIdOrAlias}`. The existing endpoint of `/_matrix/client/unstable/im.nheko.summary/rooms/{roomIdOrAlias}/summary` is deprecated. ([\#17078](https://github.com/element-hq/synapse/issues/17078))
|
||||||
|
- Apply user email & picture during OIDC registration if present & selected. ([\#17120](https://github.com/element-hq/synapse/issues/17120))
|
||||||
|
- Improve error message for cross signing reset with [MSC3861](https://github.com/matrix-org/matrix-spec-proposals/pull/3861) enabled. ([\#17121](https://github.com/element-hq/synapse/issues/17121))
|
||||||
|
- Fix a bug which meant that to-device messages received over federation could be dropped when the server was under load or networking problems caused problems between Synapse processes or the database. ([\#17127](https://github.com/element-hq/synapse/issues/17127))
|
||||||
|
- Fix bug where `StreamChangeCache` would not respect configured cache factors. ([\#17152](https://github.com/element-hq/synapse/issues/17152))
|
||||||
|
|
||||||
|
### Updates to the Docker image
|
||||||
|
|
||||||
|
- Correct licensing metadata on Docker image. ([\#17141](https://github.com/element-hq/synapse/issues/17141))
|
||||||
|
|
||||||
|
### Improved Documentation
|
||||||
|
|
||||||
|
- Update the `event_cache_size` and `global_factor` configuration options' documentation. ([\#17071](https://github.com/element-hq/synapse/issues/17071))
|
||||||
|
- Remove broken sphinx docs. ([\#17073](https://github.com/element-hq/synapse/issues/17073), [\#17148](https://github.com/element-hq/synapse/issues/17148))
|
||||||
|
- Add RuntimeDirectory to example matrix-synapse.service systemd unit. ([\#17084](https://github.com/element-hq/synapse/issues/17084))
|
||||||
|
- Fix various small typos throughout the docs. ([\#17114](https://github.com/element-hq/synapse/issues/17114))
|
||||||
|
- Update enable_notifs configuration documentation. ([\#17116](https://github.com/element-hq/synapse/issues/17116))
|
||||||
|
- Update the Upgrade Notes with the latest minimum supported Rust version of 1.66.0. Contributed by @jahway603. ([\#17140](https://github.com/element-hq/synapse/issues/17140))
|
||||||
|
|
||||||
|
### Internal Changes
|
||||||
|
|
||||||
|
- Enable [MSC3266](https://github.com/matrix-org/matrix-spec-proposals/pull/3266) by default in the Synapse Complement image. ([\#17105](https://github.com/element-hq/synapse/issues/17105))
|
||||||
|
- Add optimisation to `StreamChangeCache.get_entities_changed(..)`. ([\#17130](https://github.com/element-hq/synapse/issues/17130))
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
### Updates to locked dependencies
|
||||||
|
|
||||||
|
* Bump furo from 2024.1.29 to 2024.4.27. ([\#17133](https://github.com/element-hq/synapse/issues/17133))
|
||||||
|
* Bump idna from 3.6 to 3.7. ([\#17136](https://github.com/element-hq/synapse/issues/17136))
|
||||||
|
* Bump jsonschema from 4.21.1 to 4.22.0. ([\#17157](https://github.com/element-hq/synapse/issues/17157))
|
||||||
|
* Bump lxml from 5.1.0 to 5.2.1. ([\#17158](https://github.com/element-hq/synapse/issues/17158))
|
||||||
|
* Bump phonenumbers from 8.13.29 to 8.13.35. ([\#17106](https://github.com/element-hq/synapse/issues/17106))
|
||||||
|
- Bump pillow from 10.2.0 to 10.3.0. ([\#17146](https://github.com/element-hq/synapse/issues/17146))
|
||||||
|
* Bump pydantic from 2.6.4 to 2.7.0. ([\#17107](https://github.com/element-hq/synapse/issues/17107))
|
||||||
|
* Bump pydantic from 2.7.0 to 2.7.1. ([\#17160](https://github.com/element-hq/synapse/issues/17160))
|
||||||
|
* Bump pyicu from 2.12 to 2.13. ([\#17109](https://github.com/element-hq/synapse/issues/17109))
|
||||||
|
* Bump serde from 1.0.197 to 1.0.198. ([\#17111](https://github.com/element-hq/synapse/issues/17111))
|
||||||
|
* Bump serde from 1.0.198 to 1.0.199. ([\#17132](https://github.com/element-hq/synapse/issues/17132))
|
||||||
|
* Bump serde from 1.0.199 to 1.0.200. ([\#17161](https://github.com/element-hq/synapse/issues/17161))
|
||||||
|
* Bump serde_json from 1.0.115 to 1.0.116. ([\#17112](https://github.com/element-hq/synapse/issues/17112))
|
||||||
|
- Update `tornado` Python dependency from 6.2 to 6.4. ([\#17131](https://github.com/element-hq/synapse/issues/17131))
|
||||||
|
* Bump twisted from 23.10.0 to 24.3.0. ([\#17135](https://github.com/element-hq/synapse/issues/17135))
|
||||||
|
* Bump types-bleach from 6.1.0.1 to 6.1.0.20240331. ([\#17110](https://github.com/element-hq/synapse/issues/17110))
|
||||||
|
* Bump types-pillow from 10.2.0.20240415 to 10.2.0.20240423. ([\#17159](https://github.com/element-hq/synapse/issues/17159))
|
||||||
|
* Bump types-setuptools from 69.0.0.20240125 to 69.5.0.20240423. ([\#17134](https://github.com/element-hq/synapse/issues/17134))
|
||||||
|
|
||||||
|
# Synapse 1.106.0 (2024-04-30)
|
||||||
|
|
||||||
|
No significant changes since 1.106.0rc1.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
# Synapse 1.106.0rc1 (2024-04-25)
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
- Send an email if the address is already bound to an user account. ([\#16819](https://github.com/element-hq/synapse/issues/16819))
|
||||||
|
- Implement the rendezvous mechanism described by [MSC4108](https://github.com/matrix-org/matrix-spec-proposals/issues/4108). ([\#17056](https://github.com/element-hq/synapse/issues/17056))
|
||||||
|
- Support delegating the rendezvous mechanism described [MSC4108](https://github.com/matrix-org/matrix-spec-proposals/issues/4108) to an external implementation. ([\#17086](https://github.com/element-hq/synapse/issues/17086))
|
||||||
|
|
||||||
|
### Bugfixes
|
||||||
|
|
||||||
|
- Add validation to ensure that the `limit` parameter on `/publicRooms` is non-negative. ([\#16920](https://github.com/element-hq/synapse/issues/16920))
|
||||||
|
- Return `400 M_NOT_JSON` upon receiving invalid JSON in query parameters across various client and admin endpoints, rather than an internal server error. ([\#16923](https://github.com/element-hq/synapse/issues/16923))
|
||||||
|
- Make the CSAPI endpoint `/keys/device_signing/upload` idempotent. ([\#16943](https://github.com/element-hq/synapse/issues/16943))
|
||||||
|
- Redact membership events if the user requested erasure upon deactivating. ([\#17076](https://github.com/element-hq/synapse/issues/17076))
|
||||||
|
|
||||||
|
### Improved Documentation
|
||||||
|
|
||||||
|
- Add a prompt in the contributing guide to manually configure icu4c. ([\#17069](https://github.com/element-hq/synapse/issues/17069))
|
||||||
|
- Clarify what part of message retention is still experimental. ([\#17099](https://github.com/element-hq/synapse/issues/17099))
|
||||||
|
|
||||||
|
### Internal Changes
|
||||||
|
|
||||||
|
- Use new receipts column to optimise receipt and push action SQL queries. Contributed by Nick @ Beeper (@fizzadar). ([\#17032](https://github.com/element-hq/synapse/issues/17032), [\#17096](https://github.com/element-hq/synapse/issues/17096))
|
||||||
|
- Fix mypy with latest Twisted release. ([\#17036](https://github.com/element-hq/synapse/issues/17036))
|
||||||
|
- Bump minimum supported Rust version to 1.66.0. ([\#17079](https://github.com/element-hq/synapse/issues/17079))
|
||||||
|
- Add helpers to transform Twisted requests to Rust http Requests/Responses. ([\#17081](https://github.com/element-hq/synapse/issues/17081))
|
||||||
|
- Fix type annotation for `visited_chains` after `mypy` upgrade. ([\#17125](https://github.com/element-hq/synapse/issues/17125))
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
### Updates to locked dependencies
|
||||||
|
|
||||||
|
* Bump anyhow from 1.0.81 to 1.0.82. ([\#17095](https://github.com/element-hq/synapse/issues/17095))
|
||||||
|
* Bump peaceiris/actions-gh-pages from 3.9.3 to 4.0.0. ([\#17087](https://github.com/element-hq/synapse/issues/17087))
|
||||||
|
* Bump peaceiris/actions-mdbook from 1.2.0 to 2.0.0. ([\#17089](https://github.com/element-hq/synapse/issues/17089))
|
||||||
|
* Bump pyasn1-modules from 0.3.0 to 0.4.0. ([\#17093](https://github.com/element-hq/synapse/issues/17093))
|
||||||
|
* Bump pygithub from 2.2.0 to 2.3.0. ([\#17092](https://github.com/element-hq/synapse/issues/17092))
|
||||||
|
* Bump ruff from 0.3.5 to 0.3.7. ([\#17094](https://github.com/element-hq/synapse/issues/17094))
|
||||||
|
* Bump sigstore/cosign-installer from 3.4.0 to 3.5.0. ([\#17088](https://github.com/element-hq/synapse/issues/17088))
|
||||||
|
* Bump twine from 4.0.2 to 5.0.0. ([\#17091](https://github.com/element-hq/synapse/issues/17091))
|
||||||
|
* Bump types-pillow from 10.2.0.20240406 to 10.2.0.20240415. ([\#17090](https://github.com/element-hq/synapse/issues/17090))
|
||||||
|
|
||||||
# Synapse 1.105.1 (2024-04-23)
|
# Synapse 1.105.1 (2024-04-23)
|
||||||
|
|
||||||
## Security advisory
|
## Security advisory
|
||||||
|
|
471
Cargo.lock
generated
471
Cargo.lock
generated
|
@ -4,36 +4,42 @@ version = 3
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "aho-corasick"
|
name = "aho-corasick"
|
||||||
version = "1.0.2"
|
version = "1.1.3"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "43f6cb1bf222025340178f382c426f13757b2960e89779dfcb319c32542a5a41"
|
checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"memchr",
|
"memchr",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "anyhow"
|
name = "anyhow"
|
||||||
version = "1.0.81"
|
version = "1.0.86"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "0952808a6c2afd1aa8947271f3a60f1a6763c7b912d210184c5149b5cf147247"
|
checksum = "b3d1d046238990b9cf5bcde22a3fb3584ee5cf65fb2765f454ed428c7a0063da"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "arc-swap"
|
name = "arc-swap"
|
||||||
version = "1.5.1"
|
version = "1.7.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "983cd8b9d4b02a6dc6ffa557262eb5858a27a0038ffffe21a0f133eaa819a164"
|
checksum = "69f7f8c3906b62b754cd5326047894316021dcfe5a194c8ea52bdd94934a3457"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "autocfg"
|
name = "autocfg"
|
||||||
version = "1.1.0"
|
version = "1.3.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa"
|
checksum = "0c4b4d0bd25bd0b74681c0ad21497610ce1b7c91b1022cd21c80c6fbdd9476b0"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "base64"
|
||||||
|
version = "0.21.7"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "bitflags"
|
name = "bitflags"
|
||||||
version = "1.3.2"
|
version = "2.5.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
|
checksum = "cf4b9d6a944f767f8e5e0db018570623c85f3d925ac718db4e06d0187adb21c1"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "blake2"
|
name = "blake2"
|
||||||
|
@ -46,19 +52,40 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "block-buffer"
|
name = "block-buffer"
|
||||||
version = "0.10.3"
|
version = "0.10.4"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "69cce20737498f97b993470a6e536b8523f0af7892a4f928cceb1ac5e52ebe7e"
|
checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"generic-array",
|
"generic-array",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "bumpalo"
|
||||||
|
version = "3.16.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "79296716171880943b8470b5f8d03aa55eb2e645a4874bdbb28adb49162e012c"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "bytes"
|
||||||
|
version = "1.6.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "514de17de45fdb8dc022b1a7975556c53c86f9f0aa5f534b98977b171857c2c9"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "cfg-if"
|
name = "cfg-if"
|
||||||
version = "1.0.0"
|
version = "1.0.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
|
checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "cpufeatures"
|
||||||
|
version = "0.2.12"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "53fe5e26ff1b7aef8bca9c6080520cfb8d9333c7568e1829cef191a9723e5504"
|
||||||
|
dependencies = [
|
||||||
|
"libc",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "crypto-common"
|
name = "crypto-common"
|
||||||
version = "0.1.6"
|
version = "0.1.6"
|
||||||
|
@ -71,9 +98,9 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "digest"
|
name = "digest"
|
||||||
version = "0.10.5"
|
version = "0.10.7"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "adfbc57365a37acbd2ebf2b64d7e69bb766e2fea813521ed536f5d0520dcf86c"
|
checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"block-buffer",
|
"block-buffer",
|
||||||
"crypto-common",
|
"crypto-common",
|
||||||
|
@ -81,15 +108,58 @@ dependencies = [
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "generic-array"
|
name = "fnv"
|
||||||
version = "0.14.6"
|
version = "1.0.7"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "bff49e947297f3312447abdca79f45f4738097cc82b06e72054d2223f601f1b9"
|
checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "generic-array"
|
||||||
|
version = "0.14.7"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"typenum",
|
"typenum",
|
||||||
"version_check",
|
"version_check",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "getrandom"
|
||||||
|
version = "0.2.15"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7"
|
||||||
|
dependencies = [
|
||||||
|
"cfg-if",
|
||||||
|
"js-sys",
|
||||||
|
"libc",
|
||||||
|
"wasi",
|
||||||
|
"wasm-bindgen",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "headers"
|
||||||
|
version = "0.4.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "322106e6bd0cba2d5ead589ddb8150a13d7c4217cf80d7c4f682ca994ccc6aa9"
|
||||||
|
dependencies = [
|
||||||
|
"base64",
|
||||||
|
"bytes",
|
||||||
|
"headers-core",
|
||||||
|
"http",
|
||||||
|
"httpdate",
|
||||||
|
"mime",
|
||||||
|
"sha1",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "headers-core"
|
||||||
|
version = "0.3.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "54b4a22553d4242c49fddb9ba998a99962b5cc6f22cb5a3482bec22522403ce4"
|
||||||
|
dependencies = [
|
||||||
|
"http",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "heck"
|
name = "heck"
|
||||||
version = "0.4.1"
|
version = "0.4.1"
|
||||||
|
@ -103,16 +173,42 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70"
|
checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "indoc"
|
name = "http"
|
||||||
version = "2.0.4"
|
version = "1.1.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "1e186cfbae8084e513daff4240b4797e342f988cecda4fb6c939150f96315fd8"
|
checksum = "21b9ddb458710bc376481b842f5da65cdf31522de232c1ca8146abce2a358258"
|
||||||
|
dependencies = [
|
||||||
|
"bytes",
|
||||||
|
"fnv",
|
||||||
|
"itoa",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "httpdate"
|
||||||
|
version = "1.0.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "indoc"
|
||||||
|
version = "2.0.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "b248f5224d1d606005e02c97f5aa4e88eeb230488bcc03bc9ca4d7991399f2b5"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "itoa"
|
name = "itoa"
|
||||||
version = "1.0.4"
|
version = "1.0.11"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "4217ad341ebadf8d8e724e264f13e593e0648f5b3e94b3896a5df283be015ecc"
|
checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "js-sys"
|
||||||
|
version = "0.3.69"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "29c15563dc2726973df627357ce0c9ddddbea194836909d655df6a75d2cf296d"
|
||||||
|
dependencies = [
|
||||||
|
"wasm-bindgen",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "lazy_static"
|
name = "lazy_static"
|
||||||
|
@ -122,15 +218,15 @@ checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "libc"
|
name = "libc"
|
||||||
version = "0.2.135"
|
version = "0.2.154"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "68783febc7782c6c5cb401fbda4de5a9898be1762314da0bb2c10ced61f18b0c"
|
checksum = "ae743338b92ff9146ce83992f766a31066a91a8c84a45e0e9f21e7cf6de6d346"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "lock_api"
|
name = "lock_api"
|
||||||
version = "0.4.9"
|
version = "0.4.12"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "435011366fe56583b16cf956f9df0095b405b82d76425bc8981c0e22e60ec4df"
|
checksum = "07af8b9cdd281b7915f413fa73f29ebd5d55d0d3f0155584dade1ff18cea1b17"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"autocfg",
|
"autocfg",
|
||||||
"scopeguard",
|
"scopeguard",
|
||||||
|
@ -144,30 +240,36 @@ checksum = "90ed8c1e510134f979dbc4f070f87d4313098b704861a105fe34231c70a3901c"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "memchr"
|
name = "memchr"
|
||||||
version = "2.6.3"
|
version = "2.7.2"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "8f232d6ef707e1956a43342693d2a31e72989554d58299d7a88738cc95b0d35c"
|
checksum = "6c8640c5d730cb13ebd907d8d04b52f55ac9a2eec55b440c8892f40d56c76c1d"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "memoffset"
|
name = "memoffset"
|
||||||
version = "0.9.0"
|
version = "0.9.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "5a634b1c61a95585bd15607c6ab0c4e5b226e695ff2800ba0cdccddf208c406c"
|
checksum = "488016bfae457b036d996092f6cb448677611ce4449e970ceaf42695203f218a"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"autocfg",
|
"autocfg",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "once_cell"
|
name = "mime"
|
||||||
version = "1.15.0"
|
version = "0.3.17"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "e82dad04139b71a90c080c8463fe0dc7902db5192d939bd0950f074d014339e1"
|
checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "once_cell"
|
||||||
|
version = "1.19.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "parking_lot"
|
name = "parking_lot"
|
||||||
version = "0.12.1"
|
version = "0.12.2"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "3742b2c103b9f06bc9fff0a37ff4912935851bee6d36f3c02bcc755bcfec228f"
|
checksum = "7e4af0ca4f6caed20e900d564c242b8e5d4903fdacf31d3daf527b66fe6f42fb"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"lock_api",
|
"lock_api",
|
||||||
"parking_lot_core",
|
"parking_lot_core",
|
||||||
|
@ -175,15 +277,15 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "parking_lot_core"
|
name = "parking_lot_core"
|
||||||
version = "0.9.3"
|
version = "0.9.10"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "09a279cbf25cb0757810394fbc1e359949b59e348145c643a939a525692e6929"
|
checksum = "1e401f977ab385c9e4e3ab30627d6f26d00e2c73eef317493c4ec6d468726cf8"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"cfg-if",
|
"cfg-if",
|
||||||
"libc",
|
"libc",
|
||||||
"redox_syscall",
|
"redox_syscall",
|
||||||
"smallvec",
|
"smallvec",
|
||||||
"windows-sys",
|
"windows-targets",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
@ -193,19 +295,25 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "7170ef9988bc169ba16dd36a7fa041e5c4cbeb6a35b76d4c03daded371eae7c0"
|
checksum = "7170ef9988bc169ba16dd36a7fa041e5c4cbeb6a35b76d4c03daded371eae7c0"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "proc-macro2"
|
name = "ppv-lite86"
|
||||||
version = "1.0.76"
|
version = "0.2.17"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "95fc56cda0b5c3325f5fbbd7ff9fda9e02bb00bb3dac51252d2f1bfa1cb8cc8c"
|
checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "proc-macro2"
|
||||||
|
version = "1.0.82"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "8ad3d49ab951a01fbaafe34f2ec74122942fe18a3f9814c3268f1bb72042131b"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"unicode-ident",
|
"unicode-ident",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "pyo3"
|
name = "pyo3"
|
||||||
version = "0.20.3"
|
version = "0.21.2"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "53bdbb96d49157e65d45cc287af5f32ffadd5f4761438b527b055fb0d4bb8233"
|
checksum = "a5e00b96a521718e08e03b1a622f01c8a8deb50719335de3f60b3b3950f069d8"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"cfg-if",
|
"cfg-if",
|
||||||
|
@ -222,9 +330,9 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "pyo3-build-config"
|
name = "pyo3-build-config"
|
||||||
version = "0.20.3"
|
version = "0.21.2"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "deaa5745de3f5231ce10517a1f5dd97d53e5a2fd77aa6b5842292085831d48d7"
|
checksum = "7883df5835fafdad87c0d888b266c8ec0f4c9ca48a5bed6bbb592e8dedee1b50"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"once_cell",
|
"once_cell",
|
||||||
"target-lexicon",
|
"target-lexicon",
|
||||||
|
@ -232,9 +340,9 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "pyo3-ffi"
|
name = "pyo3-ffi"
|
||||||
version = "0.20.3"
|
version = "0.21.2"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "62b42531d03e08d4ef1f6e85a2ed422eb678b8cd62b762e53891c05faf0d4afa"
|
checksum = "01be5843dc60b916ab4dad1dca6d20b9b4e6ddc8e15f50c47fe6d85f1fb97403"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"libc",
|
"libc",
|
||||||
"pyo3-build-config",
|
"pyo3-build-config",
|
||||||
|
@ -242,9 +350,9 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "pyo3-log"
|
name = "pyo3-log"
|
||||||
version = "0.9.0"
|
version = "0.10.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "4c10808ee7250403bedb24bc30c32493e93875fef7ba3e4292226fe924f398bd"
|
checksum = "2af49834b8d2ecd555177e63b273b708dea75150abc6f5341d0a6e1a9623976c"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"arc-swap",
|
"arc-swap",
|
||||||
"log",
|
"log",
|
||||||
|
@ -253,9 +361,9 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "pyo3-macros"
|
name = "pyo3-macros"
|
||||||
version = "0.20.3"
|
version = "0.21.2"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "7305c720fa01b8055ec95e484a6eca7a83c841267f0dd5280f0c8b8551d2c158"
|
checksum = "77b34069fc0682e11b31dbd10321cbf94808394c56fd996796ce45217dfac53c"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"pyo3-macros-backend",
|
"pyo3-macros-backend",
|
||||||
|
@ -265,9 +373,9 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "pyo3-macros-backend"
|
name = "pyo3-macros-backend"
|
||||||
version = "0.20.3"
|
version = "0.21.2"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "7c7e9b68bb9c3149c5b0cade5d07f953d6d125eb4337723c4ccdb665f1f96185"
|
checksum = "08260721f32db5e1a5beae69a55553f56b99bd0e1c3e6e0a5e8851a9d0f5a85c"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"heck",
|
"heck",
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
|
@ -278,9 +386,9 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "pythonize"
|
name = "pythonize"
|
||||||
version = "0.20.0"
|
version = "0.21.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "ffd1c3ef39c725d63db5f9bc455461bafd80540cb7824c61afb823501921a850"
|
checksum = "9d0664248812c38cc55a4ed07f88e4df516ce82604b93b1ffdc041aa77a6cb3c"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"pyo3",
|
"pyo3",
|
||||||
"serde",
|
"serde",
|
||||||
|
@ -288,18 +396,48 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "quote"
|
name = "quote"
|
||||||
version = "1.0.35"
|
version = "1.0.36"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "291ec9ab5efd934aaf503a6466c5d5251535d108ee747472c3977cc5acc868ef"
|
checksum = "0fa76aaf39101c457836aec0ce2316dbdc3ab723cdda1c6bd4e6ad4208acaca7"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "redox_syscall"
|
name = "rand"
|
||||||
version = "0.2.16"
|
version = "0.8.5"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "fb5a58c1855b4b6819d59012155603f0b22ad30cad752600aadfcb695265519a"
|
checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404"
|
||||||
|
dependencies = [
|
||||||
|
"libc",
|
||||||
|
"rand_chacha",
|
||||||
|
"rand_core",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rand_chacha"
|
||||||
|
version = "0.3.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88"
|
||||||
|
dependencies = [
|
||||||
|
"ppv-lite86",
|
||||||
|
"rand_core",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rand_core"
|
||||||
|
version = "0.6.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c"
|
||||||
|
dependencies = [
|
||||||
|
"getrandom",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "redox_syscall"
|
||||||
|
version = "0.5.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "469052894dcb553421e483e4209ee581a45100d31b4018de03e5a7ad86374a7e"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bitflags",
|
"bitflags",
|
||||||
]
|
]
|
||||||
|
@ -318,9 +456,9 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "regex-automata"
|
name = "regex-automata"
|
||||||
version = "0.4.4"
|
version = "0.4.6"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "3b7fa1134405e2ec9353fd416b17f8dacd46c473d7d3fd1cf202706a14eb792a"
|
checksum = "86b83b8b9847f9bf95ef68afb0b8e6cdb80f498442f5179a29fad448fcc1eaea"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"aho-corasick",
|
"aho-corasick",
|
||||||
"memchr",
|
"memchr",
|
||||||
|
@ -329,36 +467,36 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "regex-syntax"
|
name = "regex-syntax"
|
||||||
version = "0.8.2"
|
version = "0.8.3"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "c08c74e62047bb2de4ff487b251e4a92e24f48745648451635cec7d591162d9f"
|
checksum = "adad44e29e4c806119491a7f06f03de4d1af22c3a680dd47f1e6e179439d1f56"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "ryu"
|
name = "ryu"
|
||||||
version = "1.0.11"
|
version = "1.0.18"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "4501abdff3ae82a1c1b477a17252eb69cee9e66eb915c1abaa4f44d873df9f09"
|
checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "scopeguard"
|
name = "scopeguard"
|
||||||
version = "1.1.0"
|
version = "1.2.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd"
|
checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "serde"
|
name = "serde"
|
||||||
version = "1.0.197"
|
version = "1.0.203"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "3fb1c873e1b9b056a4dc4c0c198b24c3ffa059243875552b2bd0933b1aee4ce2"
|
checksum = "7253ab4de971e72fb7be983802300c30b5a7f0c2e56fab8abfc6a214307c0094"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"serde_derive",
|
"serde_derive",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "serde_derive"
|
name = "serde_derive"
|
||||||
version = "1.0.197"
|
version = "1.0.203"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "7eb0b34b42edc17f6b7cac84a52a1c5f0e1bb2227e997ca9011ea3dd34e8610b"
|
checksum = "500cbc0ebeb6f46627f50f3f5811ccf6bf00643be300b4c3eabc0ef55dc5b5ba"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
|
@ -367,9 +505,9 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "serde_json"
|
name = "serde_json"
|
||||||
version = "1.0.115"
|
version = "1.0.117"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "12dc5c46daa8e9fdf4f5e71b6cf9a53f2487da0e86e55808e2d35539666497dd"
|
checksum = "455182ea6142b14f93f4bc5320a2b31c1f266b66a4a5c858b013302a5d8cbfc3"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"itoa",
|
"itoa",
|
||||||
"ryu",
|
"ryu",
|
||||||
|
@ -377,22 +515,44 @@ dependencies = [
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "smallvec"
|
name = "sha1"
|
||||||
version = "1.10.0"
|
version = "0.10.6"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "a507befe795404456341dfab10cef66ead4c041f62b8b11bbb92bffe5d0953e0"
|
checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba"
|
||||||
|
dependencies = [
|
||||||
|
"cfg-if",
|
||||||
|
"cpufeatures",
|
||||||
|
"digest",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "sha2"
|
||||||
|
version = "0.10.8"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "793db75ad2bcafc3ffa7c68b215fee268f537982cd901d132f89c6343f3a3dc8"
|
||||||
|
dependencies = [
|
||||||
|
"cfg-if",
|
||||||
|
"cpufeatures",
|
||||||
|
"digest",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "smallvec"
|
||||||
|
version = "1.13.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "subtle"
|
name = "subtle"
|
||||||
version = "2.4.1"
|
version = "2.5.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "6bdef32e8150c2a081110b42772ffe7d7c9032b606bc226c8260fd97e0976601"
|
checksum = "81cdd64d312baedb58e21336b31bc043b77e01cc99033ce76ef539f78e965ebc"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "syn"
|
name = "syn"
|
||||||
version = "2.0.48"
|
version = "2.0.61"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "0f3531638e407dfc0814761abb7c00a5b54992b849452a0646b7f65c9f770f3f"
|
checksum = "c993ed8ccba56ae856363b1845da7266a7cb78e1d146c8a32d54b45a8b831fc9"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
|
@ -404,35 +564,53 @@ name = "synapse"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
|
"base64",
|
||||||
"blake2",
|
"blake2",
|
||||||
|
"bytes",
|
||||||
|
"headers",
|
||||||
"hex",
|
"hex",
|
||||||
|
"http",
|
||||||
"lazy_static",
|
"lazy_static",
|
||||||
"log",
|
"log",
|
||||||
|
"mime",
|
||||||
"pyo3",
|
"pyo3",
|
||||||
"pyo3-log",
|
"pyo3-log",
|
||||||
"pythonize",
|
"pythonize",
|
||||||
"regex",
|
"regex",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
|
"sha2",
|
||||||
|
"ulid",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "target-lexicon"
|
name = "target-lexicon"
|
||||||
version = "0.12.4"
|
version = "0.12.14"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "c02424087780c9b71cc96799eaeddff35af2bc513278cda5c99fc1f5d026d3c1"
|
checksum = "e1fc403891a21bcfb7c37834ba66a547a8f402146eba7265b5a6d88059c9ff2f"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "typenum"
|
name = "typenum"
|
||||||
version = "1.15.0"
|
version = "1.17.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "dcf81ac59edc17cc8697ff311e8f5ef2d99fcbd9817b34cec66f90b6c3dfd987"
|
checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "ulid"
|
||||||
|
version = "1.1.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "34778c17965aa2a08913b57e1f34db9b4a63f5de31768b55bf20d2795f921259"
|
||||||
|
dependencies = [
|
||||||
|
"getrandom",
|
||||||
|
"rand",
|
||||||
|
"web-time",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "unicode-ident"
|
name = "unicode-ident"
|
||||||
version = "1.0.5"
|
version = "1.0.12"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "6ceab39d59e4c9499d4e5a8ee0e2735b891bb7308ac83dfb4e80cad195c9f6f3"
|
checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "unindent"
|
name = "unindent"
|
||||||
|
@ -447,44 +625,135 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f"
|
checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "windows-sys"
|
name = "wasi"
|
||||||
version = "0.36.1"
|
version = "0.11.0+wasi-snapshot-preview1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "ea04155a16a59f9eab786fe12a4a450e75cdb175f9e0d80da1e17db09f55b8d2"
|
checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "wasm-bindgen"
|
||||||
|
version = "0.2.92"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "4be2531df63900aeb2bca0daaaddec08491ee64ceecbee5076636a3b026795a8"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
"cfg-if",
|
||||||
|
"wasm-bindgen-macro",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "wasm-bindgen-backend"
|
||||||
|
version = "0.2.92"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "614d787b966d3989fa7bb98a654e369c762374fd3213d212cfc0251257e747da"
|
||||||
|
dependencies = [
|
||||||
|
"bumpalo",
|
||||||
|
"log",
|
||||||
|
"once_cell",
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn",
|
||||||
|
"wasm-bindgen-shared",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "wasm-bindgen-macro"
|
||||||
|
version = "0.2.92"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "a1f8823de937b71b9460c0c34e25f3da88250760bec0ebac694b49997550d726"
|
||||||
|
dependencies = [
|
||||||
|
"quote",
|
||||||
|
"wasm-bindgen-macro-support",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "wasm-bindgen-macro-support"
|
||||||
|
version = "0.2.92"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "e94f17b526d0a461a191c78ea52bbce64071ed5c04c9ffe424dcb38f74171bb7"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn",
|
||||||
|
"wasm-bindgen-backend",
|
||||||
|
"wasm-bindgen-shared",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "wasm-bindgen-shared"
|
||||||
|
version = "0.2.92"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "af190c94f2773fdb3729c55b007a722abb5384da03bc0986df4c289bf5567e96"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "web-time"
|
||||||
|
version = "1.1.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb"
|
||||||
|
dependencies = [
|
||||||
|
"js-sys",
|
||||||
|
"wasm-bindgen",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows-targets"
|
||||||
|
version = "0.52.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "6f0713a46559409d202e70e28227288446bf7841d3211583a4b53e3f6d96e7eb"
|
||||||
|
dependencies = [
|
||||||
|
"windows_aarch64_gnullvm",
|
||||||
"windows_aarch64_msvc",
|
"windows_aarch64_msvc",
|
||||||
"windows_i686_gnu",
|
"windows_i686_gnu",
|
||||||
|
"windows_i686_gnullvm",
|
||||||
"windows_i686_msvc",
|
"windows_i686_msvc",
|
||||||
"windows_x86_64_gnu",
|
"windows_x86_64_gnu",
|
||||||
|
"windows_x86_64_gnullvm",
|
||||||
"windows_x86_64_msvc",
|
"windows_x86_64_msvc",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "windows_aarch64_msvc"
|
name = "windows_aarch64_gnullvm"
|
||||||
version = "0.36.1"
|
version = "0.52.5"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "9bb8c3fd39ade2d67e9874ac4f3db21f0d710bee00fe7cab16949ec184eeaa47"
|
checksum = "7088eed71e8b8dda258ecc8bac5fb1153c5cffaf2578fc8ff5d61e23578d3263"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows_aarch64_msvc"
|
||||||
|
version = "0.52.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "9985fd1504e250c615ca5f281c3f7a6da76213ebd5ccc9561496568a2752afb6"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "windows_i686_gnu"
|
name = "windows_i686_gnu"
|
||||||
version = "0.36.1"
|
version = "0.52.5"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "180e6ccf01daf4c426b846dfc66db1fc518f074baa793aa7d9b9aaeffad6a3b6"
|
checksum = "88ba073cf16d5372720ec942a8ccbf61626074c6d4dd2e745299726ce8b89670"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows_i686_gnullvm"
|
||||||
|
version = "0.52.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "87f4261229030a858f36b459e748ae97545d6f1ec60e5e0d6a3d32e0dc232ee9"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "windows_i686_msvc"
|
name = "windows_i686_msvc"
|
||||||
version = "0.36.1"
|
version = "0.52.5"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "e2e7917148b2812d1eeafaeb22a97e4813dfa60a3f8f78ebe204bcc88f12f024"
|
checksum = "db3c2bf3d13d5b658be73463284eaf12830ac9a26a90c717b7f771dfe97487bf"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "windows_x86_64_gnu"
|
name = "windows_x86_64_gnu"
|
||||||
version = "0.36.1"
|
version = "0.52.5"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "4dcd171b8776c41b97521e5da127a2d86ad280114807d0b2ab1e462bc764d9e1"
|
checksum = "4e4246f76bdeff09eb48875a0fd3e2af6aada79d409d33011886d3e1581517d9"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows_x86_64_gnullvm"
|
||||||
|
version = "0.52.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "852298e482cd67c356ddd9570386e2862b5673c85bd5f88df9ab6802b334c596"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "windows_x86_64_msvc"
|
name = "windows_x86_64_msvc"
|
||||||
version = "0.36.1"
|
version = "0.52.5"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "c811ca4a8c853ef420abd8592ba53ddbbac90410fab6903b3e79972a631f7680"
|
checksum = "bec47e5bfd1bff0eeaf6d8b485cc1074891a197ab4225d504cb7a1ab88b02bf0"
|
||||||
|
|
61
Dockerfile
Normal file
61
Dockerfile
Normal file
|
@ -0,0 +1,61 @@
|
||||||
|
ARG PYTHON_VERSION=3.11
|
||||||
|
|
||||||
|
FROM docker.io/python:${PYTHON_VERSION}-slim as builder
|
||||||
|
|
||||||
|
RUN apt-get update && apt-get install -y \
|
||||||
|
build-essential \
|
||||||
|
libffi-dev \
|
||||||
|
libjpeg-dev \
|
||||||
|
libpq-dev \
|
||||||
|
libssl-dev \
|
||||||
|
libwebp-dev \
|
||||||
|
libxml++2.6-dev \
|
||||||
|
libxslt1-dev \
|
||||||
|
zlib1g-dev \
|
||||||
|
openssl \
|
||||||
|
git \
|
||||||
|
curl \
|
||||||
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
|
ENV RUSTUP_HOME=/rust
|
||||||
|
ENV CARGO_HOME=/cargo
|
||||||
|
ENV PATH=/cargo/bin:/rust/bin:$PATH
|
||||||
|
RUN mkdir /rust /cargo
|
||||||
|
|
||||||
|
RUN curl -sSf https://sh.rustup.rs | sh -s -- -y --no-modify-path --default-toolchain stable
|
||||||
|
|
||||||
|
COPY synapse /synapse/synapse/
|
||||||
|
COPY rust /synapse/rust/
|
||||||
|
COPY README.rst pyproject.toml requirements.txt build_rust.py /synapse/
|
||||||
|
|
||||||
|
RUN pip install --prefix="/install" --no-warn-script-location --ignore-installed \
|
||||||
|
--no-deps -r /synapse/requirements.txt \
|
||||||
|
&& pip install --prefix="/install" --no-warn-script-location \
|
||||||
|
--no-deps \
|
||||||
|
'git+https://github.com/maunium/synapse-simple-antispam#egg=synapse-simple-antispam' \
|
||||||
|
'git+https://github.com/devture/matrix-synapse-shared-secret-auth@2.0.3#egg=shared_secret_authenticator' \
|
||||||
|
&& pip install --prefix="/install" --no-warn-script-location \
|
||||||
|
--no-deps /synapse
|
||||||
|
|
||||||
|
FROM docker.io/python:${PYTHON_VERSION}-slim
|
||||||
|
|
||||||
|
RUN apt-get update && apt-get install -y \
|
||||||
|
curl \
|
||||||
|
libjpeg62-turbo \
|
||||||
|
libpq5 \
|
||||||
|
libwebp7 \
|
||||||
|
xmlsec1 \
|
||||||
|
libjemalloc2 \
|
||||||
|
openssl \
|
||||||
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
|
COPY --from=builder /install /usr/local
|
||||||
|
|
||||||
|
VOLUME ["/data"]
|
||||||
|
ENV LD_PRELOAD="/usr/lib/x86_64-linux-gnu/libjemalloc.so.2"
|
||||||
|
|
||||||
|
ENTRYPOINT ["python3", "-m", "synapse.app.homeserver"]
|
||||||
|
CMD ["--keys-directory", "/data", "-c", "/data/homeserver.yaml"]
|
||||||
|
|
||||||
|
HEALTHCHECK --start-period=5s --interval=1m --timeout=5s \
|
||||||
|
CMD curl -fSs http://localhost:8008/health || exit 1
|
63
README.md
Normal file
63
README.md
Normal file
|
@ -0,0 +1,63 @@
|
||||||
|
# Maunium Synapse
|
||||||
|
This is a fork of [Synapse] to remove dumb limits and fix bugs that the
|
||||||
|
upstream devs don't want to fix.
|
||||||
|
|
||||||
|
The only official distribution is the docker image in the [GitLab container
|
||||||
|
registry], but you can also install from source ([upstream instructions]).
|
||||||
|
|
||||||
|
The master branch and `:latest` docker tag are upgraded to each upstream
|
||||||
|
release candidate very soon after release (usually within 10 minutes†). There
|
||||||
|
are also docker tags for each release, e.g. `:1.75.0`. If you don't want RCs,
|
||||||
|
use the specific release tags.
|
||||||
|
|
||||||
|
†If there are merge conflicts, the update may be delayed for up to a few days
|
||||||
|
after the full release.
|
||||||
|
|
||||||
|
[Synapse]: https://github.com/matrix-org/synapse
|
||||||
|
[GitLab container registry]: https://mau.dev/maunium/synapse/container_registry
|
||||||
|
[upstream instructions]: https://github.com/matrix-org/synapse/blob/develop/INSTALL.md#installing-from-source
|
||||||
|
|
||||||
|
## List of changes
|
||||||
|
* Default power level for room creator is 9001 instead of 100.
|
||||||
|
* Room creator can specify a custom room ID with the `room_id` param in the
|
||||||
|
request body. If the room ID is already in use, it will return `M_CONFLICT`.
|
||||||
|
* ~~URL previewer user agent includes `Bot` so Twitter previews work properly.~~
|
||||||
|
Upstreamed after over 2 years 🎉
|
||||||
|
* ~~Local event creation concurrency is disabled to avoid unnecessary state
|
||||||
|
resolution.~~ Upstreamed after over 3 years 🎉
|
||||||
|
* Register admin API can register invalid user IDs.
|
||||||
|
* Docker image with jemalloc enabled by default.
|
||||||
|
* Config option to allow specific users to send events without unnecessary
|
||||||
|
validation.
|
||||||
|
* Config option to allow specific users to receive events that are usually
|
||||||
|
filtered away (e.g. `org.matrix.dummy_event` and `m.room.aliases`).
|
||||||
|
* Config option to allow specific users to use timestamp massaging without
|
||||||
|
being appservice users.
|
||||||
|
* Removed bad pusher URL validation.
|
||||||
|
* webp images are thumbnailed to webp instead of jpeg to avoid losing
|
||||||
|
transparency.
|
||||||
|
* Media repo `Cache-Control` header says `immutable` and 1 year for all media
|
||||||
|
that exists, as media IDs in Matrix are immutable.
|
||||||
|
* Allowed sending custom data with read receipts.
|
||||||
|
|
||||||
|
You can view the full list of changes on the [meow-patchset] branch.
|
||||||
|
Additionally, historical patch sets are saved as `meow-patchset-vX` [tags].
|
||||||
|
|
||||||
|
[meow-patchset]: https://mau.dev/maunium/synapse/-/compare/patchset-base...meow-patchset
|
||||||
|
[tags]: https://mau.dev/maunium/synapse/-/tags?search=meow-patchset&sort=updated_desc
|
||||||
|
|
||||||
|
## Configuration reference
|
||||||
|
```yaml
|
||||||
|
meow:
|
||||||
|
# List of users who aren't subject to unnecessary validation in the C-S API.
|
||||||
|
validation_override:
|
||||||
|
- "@you:example.com"
|
||||||
|
# List of users who will get org.matrix.dummy_event and m.room.aliases events down /sync
|
||||||
|
filter_override:
|
||||||
|
- "@you:example.com"
|
||||||
|
# Whether or not the admin API should be able to register invalid user IDs.
|
||||||
|
admin_api_register_invalid: true
|
||||||
|
# List of users who can use timestamp massaging without being appservices
|
||||||
|
timestamp_override:
|
||||||
|
- "@you:example.com"
|
||||||
|
```
|
42
debian/changelog
vendored
42
debian/changelog
vendored
|
@ -1,3 +1,45 @@
|
||||||
|
matrix-synapse-py3 (1.109.0~rc1) stable; urgency=medium
|
||||||
|
|
||||||
|
* New Synapse release 1.109.0rc1.
|
||||||
|
|
||||||
|
-- Synapse Packaging team <packages@matrix.org> Tue, 04 Jun 2024 09:42:46 +0100
|
||||||
|
|
||||||
|
matrix-synapse-py3 (1.108.0) stable; urgency=medium
|
||||||
|
|
||||||
|
* New Synapse release 1.108.0.
|
||||||
|
|
||||||
|
-- Synapse Packaging team <packages@matrix.org> Tue, 28 May 2024 11:54:22 +0100
|
||||||
|
|
||||||
|
matrix-synapse-py3 (1.108.0~rc1) stable; urgency=medium
|
||||||
|
|
||||||
|
* New Synapse release 1.108.0rc1.
|
||||||
|
|
||||||
|
-- Synapse Packaging team <packages@matrix.org> Tue, 21 May 2024 10:54:13 +0100
|
||||||
|
|
||||||
|
matrix-synapse-py3 (1.107.0) stable; urgency=medium
|
||||||
|
|
||||||
|
* New Synapse release 1.107.0.
|
||||||
|
|
||||||
|
-- Synapse Packaging team <packages@matrix.org> Tue, 14 May 2024 14:15:34 +0100
|
||||||
|
|
||||||
|
matrix-synapse-py3 (1.107.0~rc1) stable; urgency=medium
|
||||||
|
|
||||||
|
* New Synapse release 1.107.0rc1.
|
||||||
|
|
||||||
|
-- Synapse Packaging team <packages@matrix.org> Tue, 07 May 2024 16:26:26 +0100
|
||||||
|
|
||||||
|
matrix-synapse-py3 (1.106.0) stable; urgency=medium
|
||||||
|
|
||||||
|
* New Synapse release 1.106.0.
|
||||||
|
|
||||||
|
-- Synapse Packaging team <packages@matrix.org> Tue, 30 Apr 2024 11:51:43 +0100
|
||||||
|
|
||||||
|
matrix-synapse-py3 (1.106.0~rc1) stable; urgency=medium
|
||||||
|
|
||||||
|
* New Synapse release 1.106.0rc1.
|
||||||
|
|
||||||
|
-- Synapse Packaging team <packages@matrix.org> Thu, 25 Apr 2024 15:54:59 +0100
|
||||||
|
|
||||||
matrix-synapse-py3 (1.105.1) stable; urgency=medium
|
matrix-synapse-py3 (1.105.1) stable; urgency=medium
|
||||||
|
|
||||||
* New Synapse release 1.105.1.
|
* New Synapse release 1.105.1.
|
||||||
|
|
|
@ -1,20 +0,0 @@
|
||||||
# Minimal makefile for Sphinx documentation
|
|
||||||
#
|
|
||||||
|
|
||||||
# You can set these variables from the command line, and also
|
|
||||||
# from the environment for the first two.
|
|
||||||
SPHINXOPTS ?=
|
|
||||||
SPHINXBUILD ?= sphinx-build
|
|
||||||
SOURCEDIR = .
|
|
||||||
BUILDDIR = _build
|
|
||||||
|
|
||||||
# Put it first so that "make" without argument is like "make help".
|
|
||||||
help:
|
|
||||||
@$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
|
|
||||||
|
|
||||||
.PHONY: help Makefile
|
|
||||||
|
|
||||||
# Catch-all target: route all unknown targets to Sphinx using the new
|
|
||||||
# "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS).
|
|
||||||
%: Makefile
|
|
||||||
@$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
|
|
|
@ -1,50 +0,0 @@
|
||||||
# Configuration file for the Sphinx documentation builder.
|
|
||||||
#
|
|
||||||
# For the full list of built-in configuration values, see the documentation:
|
|
||||||
# https://www.sphinx-doc.org/en/master/usage/configuration.html
|
|
||||||
|
|
||||||
# -- Project information -----------------------------------------------------
|
|
||||||
# https://www.sphinx-doc.org/en/master/usage/configuration.html#project-information
|
|
||||||
|
|
||||||
project = "Synapse development"
|
|
||||||
copyright = "2023, The Matrix.org Foundation C.I.C."
|
|
||||||
author = "The Synapse Maintainers and Community"
|
|
||||||
|
|
||||||
# -- General configuration ---------------------------------------------------
|
|
||||||
# https://www.sphinx-doc.org/en/master/usage/configuration.html#general-configuration
|
|
||||||
|
|
||||||
extensions = [
|
|
||||||
"autodoc2",
|
|
||||||
"myst_parser",
|
|
||||||
]
|
|
||||||
|
|
||||||
templates_path = ["_templates"]
|
|
||||||
exclude_patterns = ["_build", "Thumbs.db", ".DS_Store"]
|
|
||||||
|
|
||||||
|
|
||||||
# -- Options for Autodoc2 ----------------------------------------------------
|
|
||||||
|
|
||||||
autodoc2_docstring_parser_regexes = [
|
|
||||||
# this will render all docstrings as 'MyST' Markdown
|
|
||||||
(r".*", "myst"),
|
|
||||||
]
|
|
||||||
|
|
||||||
autodoc2_packages = [
|
|
||||||
{
|
|
||||||
"path": "../synapse",
|
|
||||||
# Don't render documentation for everything as a matter of course
|
|
||||||
"auto_mode": False,
|
|
||||||
},
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
# -- Options for MyST (Markdown) ---------------------------------------------
|
|
||||||
|
|
||||||
# myst_heading_anchors = 2
|
|
||||||
|
|
||||||
|
|
||||||
# -- Options for HTML output -------------------------------------------------
|
|
||||||
# https://www.sphinx-doc.org/en/master/usage/configuration.html#options-for-html-output
|
|
||||||
|
|
||||||
html_theme = "furo"
|
|
||||||
html_static_path = ["_static"]
|
|
|
@ -1,22 +0,0 @@
|
||||||
.. Synapse Developer Documentation documentation master file, created by
|
|
||||||
sphinx-quickstart on Mon Mar 13 08:59:51 2023.
|
|
||||||
You can adapt this file completely to your liking, but it should at least
|
|
||||||
contain the root `toctree` directive.
|
|
||||||
|
|
||||||
Welcome to the Synapse Developer Documentation!
|
|
||||||
===========================================================
|
|
||||||
|
|
||||||
.. toctree::
|
|
||||||
:maxdepth: 2
|
|
||||||
:caption: Contents:
|
|
||||||
|
|
||||||
modules/federation_sender
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
Indices and tables
|
|
||||||
==================
|
|
||||||
|
|
||||||
* :ref:`genindex`
|
|
||||||
* :ref:`modindex`
|
|
||||||
* :ref:`search`
|
|
|
@ -1,5 +0,0 @@
|
||||||
Federation Sender
|
|
||||||
=================
|
|
||||||
|
|
||||||
```{autodoc2-docstring} synapse.federation.sender
|
|
||||||
```
|
|
|
@ -163,7 +163,7 @@ FROM docker.io/library/python:${PYTHON_VERSION}-slim-bookworm
|
||||||
LABEL org.opencontainers.image.url='https://matrix.org/docs/projects/server/synapse'
|
LABEL org.opencontainers.image.url='https://matrix.org/docs/projects/server/synapse'
|
||||||
LABEL org.opencontainers.image.documentation='https://github.com/element-hq/synapse/blob/master/docker/README.md'
|
LABEL org.opencontainers.image.documentation='https://github.com/element-hq/synapse/blob/master/docker/README.md'
|
||||||
LABEL org.opencontainers.image.source='https://github.com/element-hq/synapse.git'
|
LABEL org.opencontainers.image.source='https://github.com/element-hq/synapse.git'
|
||||||
LABEL org.opencontainers.image.licenses='Apache-2.0'
|
LABEL org.opencontainers.image.licenses='AGPL-3.0-or-later'
|
||||||
|
|
||||||
RUN \
|
RUN \
|
||||||
--mount=type=cache,target=/var/cache/apt,sharing=locked \
|
--mount=type=cache,target=/var/cache/apt,sharing=locked \
|
||||||
|
|
|
@ -92,8 +92,6 @@ allow_device_name_lookup_over_federation: true
|
||||||
## Experimental Features ##
|
## Experimental Features ##
|
||||||
|
|
||||||
experimental_features:
|
experimental_features:
|
||||||
# client-side support for partial state in /send_join responses
|
|
||||||
faster_joins: true
|
|
||||||
# Enable support for polls
|
# Enable support for polls
|
||||||
msc3381_polls_enabled: true
|
msc3381_polls_enabled: true
|
||||||
# Enable deleting device-specific notification settings stored in account data
|
# Enable deleting device-specific notification settings stored in account data
|
||||||
|
@ -102,6 +100,12 @@ experimental_features:
|
||||||
msc3391_enabled: true
|
msc3391_enabled: true
|
||||||
# Filtering /messages by relation type.
|
# Filtering /messages by relation type.
|
||||||
msc3874_enabled: true
|
msc3874_enabled: true
|
||||||
|
# no UIA for x-signing upload for the first time
|
||||||
|
msc3967_enabled: true
|
||||||
|
# Expose a room summary for public rooms
|
||||||
|
msc3266_enabled: true
|
||||||
|
|
||||||
|
msc4115_membership_on_events: true
|
||||||
|
|
||||||
server_notices:
|
server_notices:
|
||||||
system_mxid_localpart: _server
|
system_mxid_localpart: _server
|
||||||
|
|
|
@ -211,6 +211,8 @@ WORKERS_CONFIG: Dict[str, Dict[str, Any]] = {
|
||||||
"^/_matrix/federation/(v1|v2)/make_leave/",
|
"^/_matrix/federation/(v1|v2)/make_leave/",
|
||||||
"^/_matrix/federation/(v1|v2)/send_join/",
|
"^/_matrix/federation/(v1|v2)/send_join/",
|
||||||
"^/_matrix/federation/(v1|v2)/send_leave/",
|
"^/_matrix/federation/(v1|v2)/send_leave/",
|
||||||
|
"^/_matrix/federation/v1/make_knock/",
|
||||||
|
"^/_matrix/federation/v1/send_knock/",
|
||||||
"^/_matrix/federation/(v1|v2)/invite/",
|
"^/_matrix/federation/(v1|v2)/invite/",
|
||||||
"^/_matrix/federation/(v1|v2)/query_auth/",
|
"^/_matrix/federation/(v1|v2)/query_auth/",
|
||||||
"^/_matrix/federation/(v1|v2)/event_auth/",
|
"^/_matrix/federation/(v1|v2)/event_auth/",
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
# Edit Room Membership API
|
# Edit Room Membership API
|
||||||
|
|
||||||
This API allows an administrator to join an user account with a given `user_id`
|
This API allows an administrator to join a user account with a given `user_id`
|
||||||
to a room with a given `room_id_or_alias`. You can only modify the membership of
|
to a room with a given `room_id_or_alias`. You can only modify the membership of
|
||||||
local users. The server administrator must be in the room and have permission to
|
local users. The server administrator must be in the room and have permission to
|
||||||
invite users.
|
invite users.
|
||||||
|
|
|
@ -141,8 +141,8 @@ Body parameters:
|
||||||
provider for SSO (Single sign-on). More details are in the configuration manual under the
|
provider for SSO (Single sign-on). More details are in the configuration manual under the
|
||||||
sections [sso](../usage/configuration/config_documentation.md#sso) and [oidc_providers](../usage/configuration/config_documentation.md#oidc_providers).
|
sections [sso](../usage/configuration/config_documentation.md#sso) and [oidc_providers](../usage/configuration/config_documentation.md#oidc_providers).
|
||||||
- `auth_provider` - **string**, required. The unique, internal ID of the external identity provider.
|
- `auth_provider` - **string**, required. The unique, internal ID of the external identity provider.
|
||||||
The same as `idp_id` from the homeserver configuration. Note that no error is raised if the
|
The same as `idp_id` from the homeserver configuration. If using OIDC, this value should be prefixed
|
||||||
provided value is not in the homeserver configuration.
|
with `oidc-`. Note that no error is raised if the provided value is not in the homeserver configuration.
|
||||||
- `external_id` - **string**, required. An identifier for the user in the external identity provider.
|
- `external_id` - **string**, required. An identifier for the user in the external identity provider.
|
||||||
When the user logs in to the identity provider, this must be the unique ID that they map to.
|
When the user logs in to the identity provider, this must be the unique ID that they map to.
|
||||||
- `admin` - **bool**, optional, defaults to `false`. Whether the user is a homeserver administrator,
|
- `admin` - **bool**, optional, defaults to `false`. Whether the user is a homeserver administrator,
|
||||||
|
|
|
@ -86,6 +86,8 @@ poetry install --extras all
|
||||||
This will install the runtime and developer dependencies for the project. Be sure to check
|
This will install the runtime and developer dependencies for the project. Be sure to check
|
||||||
that the `poetry install` step completed cleanly.
|
that the `poetry install` step completed cleanly.
|
||||||
|
|
||||||
|
For OSX users, be sure to set `PKG_CONFIG_PATH` to support `icu4c`. Run `brew info icu4c` for more details.
|
||||||
|
|
||||||
## Running Synapse via poetry
|
## Running Synapse via poetry
|
||||||
|
|
||||||
To start a local instance of Synapse in the locked poetry environment, create a config file:
|
To start a local instance of Synapse in the locked poetry environment, create a config file:
|
||||||
|
|
|
@ -7,8 +7,10 @@ follow the semantics described in
|
||||||
and allow server and room admins to configure how long messages should
|
and allow server and room admins to configure how long messages should
|
||||||
be kept in a homeserver's database before being purged from it.
|
be kept in a homeserver's database before being purged from it.
|
||||||
**Please note that, as this feature isn't part of the Matrix
|
**Please note that, as this feature isn't part of the Matrix
|
||||||
specification yet, this implementation is to be considered as
|
specification yet, the use of `m.room.retention` events for per-room
|
||||||
experimental.**
|
retention policies is to be considered as experimental. However, the use
|
||||||
|
of a default message retention policy is considered a stable feature
|
||||||
|
in Synapse.**
|
||||||
|
|
||||||
A message retention policy is mainly defined by its `max_lifetime`
|
A message retention policy is mainly defined by its `max_lifetime`
|
||||||
parameter, which defines how long a message can be kept around after
|
parameter, which defines how long a message can be kept around after
|
||||||
|
@ -49,8 +51,8 @@ clients.
|
||||||
|
|
||||||
## Server configuration
|
## Server configuration
|
||||||
|
|
||||||
Support for this feature can be enabled and configured by adding a the
|
Support for this feature can be enabled and configured by adding the
|
||||||
`retention` in the Synapse configuration file (see
|
`retention` option in the Synapse configuration file (see
|
||||||
[configuration manual](usage/configuration/config_documentation.md#retention)).
|
[configuration manual](usage/configuration/config_documentation.md#retention)).
|
||||||
|
|
||||||
To enable support for message retention policies, set the setting
|
To enable support for message retention policies, set the setting
|
||||||
|
@ -115,7 +117,7 @@ In this example, we define three jobs:
|
||||||
policy's `max_lifetime` is greater than a week.
|
policy's `max_lifetime` is greater than a week.
|
||||||
|
|
||||||
Note that this example is tailored to show different configurations and
|
Note that this example is tailored to show different configurations and
|
||||||
features slightly more jobs than it's probably necessary (in practice, a
|
features slightly more jobs than is probably necessary (in practice, a
|
||||||
server admin would probably consider it better to replace the two last
|
server admin would probably consider it better to replace the two last
|
||||||
jobs with one that runs once a day and handles rooms which
|
jobs with one that runs once a day and handles rooms which
|
||||||
policy's `max_lifetime` is greater than 3 days).
|
policy's `max_lifetime` is greater than 3 days).
|
||||||
|
|
|
@ -525,6 +525,8 @@ oidc_providers:
|
||||||
(`Options > Security > ID Token signature algorithm` and `Options > Security >
|
(`Options > Security > ID Token signature algorithm` and `Options > Security >
|
||||||
Access Token signature algorithm`)
|
Access Token signature algorithm`)
|
||||||
- Scopes: OpenID, Email and Profile
|
- Scopes: OpenID, Email and Profile
|
||||||
|
- Force claims into `id_token`
|
||||||
|
(`Options > Advanced > Force claims to be returned in ID Token`)
|
||||||
- Allowed redirection addresses for login (`Options > Basic > Allowed
|
- Allowed redirection addresses for login (`Options > Basic > Allowed
|
||||||
redirection addresses for login` ) :
|
redirection addresses for login` ) :
|
||||||
`[synapse public baseurl]/_synapse/client/oidc/callback`
|
`[synapse public baseurl]/_synapse/client/oidc/callback`
|
||||||
|
|
|
@ -128,7 +128,7 @@ can read more about that [here](https://www.postgresql.org/docs/10/kernel-resour
|
||||||
### Overview
|
### Overview
|
||||||
|
|
||||||
The script `synapse_port_db` allows porting an existing synapse server
|
The script `synapse_port_db` allows porting an existing synapse server
|
||||||
backed by SQLite to using PostgreSQL. This is done in as a two phase
|
backed by SQLite to using PostgreSQL. This is done as a two phase
|
||||||
process:
|
process:
|
||||||
|
|
||||||
1. Copy the existing SQLite database to a separate location and run
|
1. Copy the existing SQLite database to a separate location and run
|
||||||
|
@ -242,12 +242,11 @@ host all all ::1/128 ident
|
||||||
|
|
||||||
### Fixing incorrect `COLLATE` or `CTYPE`
|
### Fixing incorrect `COLLATE` or `CTYPE`
|
||||||
|
|
||||||
Synapse will refuse to set up a new database if it has the wrong values of
|
Synapse will refuse to start when using a database with incorrect values of
|
||||||
`COLLATE` and `CTYPE` set. Synapse will also refuse to start an existing database with incorrect values
|
`COLLATE` and `CTYPE` unless the config flag `allow_unsafe_locale`, found in the
|
||||||
of `COLLATE` and `CTYPE` unless the config flag `allow_unsafe_locale`, found in the
|
`database` section of the config, is set to true. Using different locales can
|
||||||
`database` section of the config, is set to true. Using different locales can cause issues if the locale library is updated from
|
cause issues if the locale library is updated from underneath the database, or
|
||||||
underneath the database, or if a different version of the locale is used on any
|
if a different version of the locale is used on any replicas.
|
||||||
replicas.
|
|
||||||
|
|
||||||
If you have a database with an unsafe locale, the safest way to fix the issue is to dump the database and recreate it with
|
If you have a database with an unsafe locale, the safest way to fix the issue is to dump the database and recreate it with
|
||||||
the correct locale parameter (as shown above). It is also possible to change the
|
the correct locale parameter (as shown above). It is also possible to change the
|
||||||
|
|
|
@ -259,9 +259,9 @@ users, etc.) to the developers via the `--report-stats` argument.
|
||||||
|
|
||||||
This command will generate you a config file that you can then customise, but it will
|
This command will generate you a config file that you can then customise, but it will
|
||||||
also generate a set of keys for you. These keys will allow your homeserver to
|
also generate a set of keys for you. These keys will allow your homeserver to
|
||||||
identify itself to other homeserver, so don't lose or delete them. It would be
|
identify itself to other homeservers, so don't lose or delete them. It would be
|
||||||
wise to back them up somewhere safe. (If, for whatever reason, you do need to
|
wise to back them up somewhere safe. (If, for whatever reason, you do need to
|
||||||
change your homeserver's keys, you may find that other homeserver have the
|
change your homeserver's keys, you may find that other homeservers have the
|
||||||
old key cached. If you update the signing key, you should change the name of the
|
old key cached. If you update the signing key, you should change the name of the
|
||||||
key in the `<server name>.signing.key` file (the second word) to something
|
key in the `<server name>.signing.key` file (the second word) to something
|
||||||
different. See the [spec](https://matrix.org/docs/spec/server_server/latest.html#retrieving-server-keys) for more information on key management).
|
different. See the [spec](https://matrix.org/docs/spec/server_server/latest.html#retrieving-server-keys) for more information on key management).
|
||||||
|
|
|
@ -98,6 +98,7 @@ A custom mapping provider must specify the following methods:
|
||||||
either accept this localpart or pick their own username. Otherwise this
|
either accept this localpart or pick their own username. Otherwise this
|
||||||
option has no effect. If omitted, defaults to `False`.
|
option has no effect. If omitted, defaults to `False`.
|
||||||
- `display_name`: An optional string, the display name for the user.
|
- `display_name`: An optional string, the display name for the user.
|
||||||
|
- `picture`: An optional string, the avatar url for the user.
|
||||||
- `emails`: A list of strings, the email address(es) to associate with
|
- `emails`: A list of strings, the email address(es) to associate with
|
||||||
this user. If omitted, defaults to an empty list.
|
this user. If omitted, defaults to an empty list.
|
||||||
* `async def get_extra_attributes(self, userinfo, token)`
|
* `async def get_extra_attributes(self, userinfo, token)`
|
||||||
|
|
|
@ -9,6 +9,7 @@ ReloadPropagatedFrom=matrix-synapse.target
|
||||||
Type=notify
|
Type=notify
|
||||||
NotifyAccess=main
|
NotifyAccess=main
|
||||||
User=matrix-synapse
|
User=matrix-synapse
|
||||||
|
RuntimeDirectory=synapse
|
||||||
WorkingDirectory=/var/lib/matrix-synapse
|
WorkingDirectory=/var/lib/matrix-synapse
|
||||||
EnvironmentFile=-/etc/default/matrix-synapse
|
EnvironmentFile=-/etc/default/matrix-synapse
|
||||||
ExecStartPre=/opt/venvs/matrix-synapse/bin/python -m synapse.app.homeserver --config-path=/etc/matrix-synapse/homeserver.yaml --config-path=/etc/matrix-synapse/conf.d/ --generate-keys
|
ExecStartPre=/opt/venvs/matrix-synapse/bin/python -m synapse.app.homeserver --config-path=/etc/matrix-synapse/homeserver.yaml --config-path=/etc/matrix-synapse/conf.d/ --generate-keys
|
||||||
|
|
|
@ -117,6 +117,14 @@ each upgrade are complete before moving on to the next upgrade, to avoid
|
||||||
stacking them up. You can monitor the currently running background updates with
|
stacking them up. You can monitor the currently running background updates with
|
||||||
[the Admin API](usage/administration/admin_api/background_updates.html#status).
|
[the Admin API](usage/administration/admin_api/background_updates.html#status).
|
||||||
|
|
||||||
|
# Upgrading to v1.106.0
|
||||||
|
|
||||||
|
## Minimum supported Rust version
|
||||||
|
The minimum supported Rust version has been increased from v1.65.0 to v1.66.0.
|
||||||
|
Users building from source will need to ensure their `rustc` version is up to
|
||||||
|
date.
|
||||||
|
|
||||||
|
|
||||||
# Upgrading to v1.100.0
|
# Upgrading to v1.100.0
|
||||||
|
|
||||||
## Minimum supported Rust version
|
## Minimum supported Rust version
|
||||||
|
|
|
@ -44,7 +44,7 @@ For each update:
|
||||||
|
|
||||||
## Enabled
|
## Enabled
|
||||||
|
|
||||||
This API allow pausing background updates.
|
This API allows pausing background updates.
|
||||||
|
|
||||||
Background updates should *not* be paused for significant periods of time, as
|
Background updates should *not* be paused for significant periods of time, as
|
||||||
this can affect the performance of Synapse.
|
this can affect the performance of Synapse.
|
||||||
|
|
|
@ -241,7 +241,7 @@ in memory constrained environments, or increased if performance starts to
|
||||||
degrade.
|
degrade.
|
||||||
|
|
||||||
However, degraded performance due to a low cache factor, common on
|
However, degraded performance due to a low cache factor, common on
|
||||||
machines with slow disks, often leads to explosions in memory use due
|
machines with slow disks, often leads to explosions in memory use due to
|
||||||
backlogged requests. In this case, reducing the cache factor will make
|
backlogged requests. In this case, reducing the cache factor will make
|
||||||
things worse. Instead, try increasing it drastically. 2.0 is a good
|
things worse. Instead, try increasing it drastically. 2.0 is a good
|
||||||
starting value.
|
starting value.
|
||||||
|
@ -250,10 +250,10 @@ Using [libjemalloc](https://jemalloc.net) can also yield a significant
|
||||||
improvement in overall memory use, and especially in terms of giving back
|
improvement in overall memory use, and especially in terms of giving back
|
||||||
RAM to the OS. To use it, the library must simply be put in the
|
RAM to the OS. To use it, the library must simply be put in the
|
||||||
LD_PRELOAD environment variable when launching Synapse. On Debian, this
|
LD_PRELOAD environment variable when launching Synapse. On Debian, this
|
||||||
can be done by installing the `libjemalloc1` package and adding this
|
can be done by installing the `libjemalloc2` package and adding this
|
||||||
line to `/etc/default/matrix-synapse`:
|
line to `/etc/default/matrix-synapse`:
|
||||||
|
|
||||||
LD_PRELOAD=/usr/lib/x86_64-linux-gnu/libjemalloc.so.1
|
LD_PRELOAD=/usr/lib/x86_64-linux-gnu/libjemalloc.so.2
|
||||||
|
|
||||||
This made a significant difference on Python 2.7 - it's unclear how
|
This made a significant difference on Python 2.7 - it's unclear how
|
||||||
much of an improvement it provides on Python 3.x.
|
much of an improvement it provides on Python 3.x.
|
||||||
|
|
|
@ -676,8 +676,8 @@ This setting has the following sub-options:
|
||||||
trailing 's'.
|
trailing 's'.
|
||||||
* `app_name`: `app_name` defines the default value for '%(app)s' in `notif_from` and email
|
* `app_name`: `app_name` defines the default value for '%(app)s' in `notif_from` and email
|
||||||
subjects. It defaults to 'Matrix'.
|
subjects. It defaults to 'Matrix'.
|
||||||
* `enable_notifs`: Set to true to enable sending emails for messages that the user
|
* `enable_notifs`: Set to true to allow users to receive e-mail notifications. If this is not set,
|
||||||
has missed. Disabled by default.
|
users can configure e-mail notifications but will not receive them. Disabled by default.
|
||||||
* `notif_for_new_users`: Set to false to disable automatic subscription to email
|
* `notif_for_new_users`: Set to false to disable automatic subscription to email
|
||||||
notifications for new users. Enabled by default.
|
notifications for new users. Enabled by default.
|
||||||
* `notif_delay_before_mail`: The time to wait before emailing about a notification.
|
* `notif_delay_before_mail`: The time to wait before emailing about a notification.
|
||||||
|
@ -1232,6 +1232,31 @@ federation_domain_whitelist:
|
||||||
- syd.example.com
|
- syd.example.com
|
||||||
```
|
```
|
||||||
---
|
---
|
||||||
|
### `federation_whitelist_endpoint_enabled`
|
||||||
|
|
||||||
|
Enables an endpoint for fetching the federation whitelist config.
|
||||||
|
|
||||||
|
The request method and path is `GET /_synapse/client/v1/config/federation_whitelist`, and the
|
||||||
|
response format is:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"whitelist_enabled": true, // Whether the federation whitelist is being enforced
|
||||||
|
"whitelist": [ // Which server names are allowed by the whitelist
|
||||||
|
"example.com"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
If `whitelist_enabled` is `false` then the server is permitted to federate with all others.
|
||||||
|
|
||||||
|
The endpoint requires authentication.
|
||||||
|
|
||||||
|
Example configuration:
|
||||||
|
```yaml
|
||||||
|
federation_whitelist_endpoint_enabled: true
|
||||||
|
```
|
||||||
|
---
|
||||||
### `federation_metrics_domains`
|
### `federation_metrics_domains`
|
||||||
|
|
||||||
Report prometheus metrics on the age of PDUs being sent to and received from
|
Report prometheus metrics on the age of PDUs being sent to and received from
|
||||||
|
@ -1317,6 +1342,12 @@ Options related to caching.
|
||||||
The number of events to cache in memory. Defaults to 10K. Like other caches,
|
The number of events to cache in memory. Defaults to 10K. Like other caches,
|
||||||
this is affected by `caches.global_factor` (see below).
|
this is affected by `caches.global_factor` (see below).
|
||||||
|
|
||||||
|
For example, the default is 10K and the global_factor default is 0.5.
|
||||||
|
|
||||||
|
Since 10K * 0.5 is 5K then the event cache size will be 5K.
|
||||||
|
|
||||||
|
The cache affected by this configuration is named as "*getEvent*".
|
||||||
|
|
||||||
Note that this option is not part of the `caches` section.
|
Note that this option is not part of the `caches` section.
|
||||||
|
|
||||||
Example configuration:
|
Example configuration:
|
||||||
|
@ -1342,6 +1373,8 @@ number of entries that can be stored.
|
||||||
|
|
||||||
Defaults to 0.5, which will halve the size of all caches.
|
Defaults to 0.5, which will halve the size of all caches.
|
||||||
|
|
||||||
|
Note that changing this value also affects the HTTP connection pool.
|
||||||
|
|
||||||
* `per_cache_factors`: A dictionary of cache name to cache factor for that individual
|
* `per_cache_factors`: A dictionary of cache name to cache factor for that individual
|
||||||
cache. Overrides the global cache factor for a given cache.
|
cache. Overrides the global cache factor for a given cache.
|
||||||
|
|
||||||
|
@ -2583,6 +2616,11 @@ Possible values for this option are:
|
||||||
* "trusted_private_chat": an invitation is required to join this room and the invitee is
|
* "trusted_private_chat": an invitation is required to join this room and the invitee is
|
||||||
assigned a power level of 100 upon joining the room.
|
assigned a power level of 100 upon joining the room.
|
||||||
|
|
||||||
|
Each preset will set up a room in the same manner as if it were provided as the `preset` parameter when
|
||||||
|
calling the
|
||||||
|
[`POST /_matrix/client/v3/createRoom`](https://spec.matrix.org/latest/client-server-api/#post_matrixclientv3createroom)
|
||||||
|
Client-Server API endpoint.
|
||||||
|
|
||||||
If a value of "private_chat" or "trusted_private_chat" is used then
|
If a value of "private_chat" or "trusted_private_chat" is used then
|
||||||
`auto_join_mxid_localpart` must also be configured.
|
`auto_join_mxid_localpart` must also be configured.
|
||||||
|
|
||||||
|
@ -3520,6 +3558,15 @@ Has the following sub-options:
|
||||||
users. This allows the CAS SSO flow to be limited to sign in only, rather than
|
users. This allows the CAS SSO flow to be limited to sign in only, rather than
|
||||||
automatically registering users that have a valid SSO login but do not have
|
automatically registering users that have a valid SSO login but do not have
|
||||||
a pre-registered account. Defaults to true.
|
a pre-registered account. Defaults to true.
|
||||||
|
* `allow_numeric_ids`: set to 'true' allow numeric user IDs (default false).
|
||||||
|
This allows CAS SSO flow to provide user IDs composed of numbers only.
|
||||||
|
These identifiers will be prefixed by the letter "u" by default.
|
||||||
|
The prefix can be configured using the "numeric_ids_prefix" option.
|
||||||
|
Be careful to choose the prefix correctly to avoid any possible conflicts
|
||||||
|
(e.g. user 1234 becomes u1234 when a user u1234 already exists).
|
||||||
|
* `numeric_ids_prefix`: the prefix you wish to add in front of a numeric user ID
|
||||||
|
when the "allow_numeric_ids" option is set to "true".
|
||||||
|
By default, the prefix is the letter "u" and only alphanumeric characters are allowed.
|
||||||
|
|
||||||
*Added in Synapse 1.93.0.*
|
*Added in Synapse 1.93.0.*
|
||||||
|
|
||||||
|
@ -3534,6 +3581,8 @@ cas_config:
|
||||||
userGroup: "staff"
|
userGroup: "staff"
|
||||||
department: None
|
department: None
|
||||||
enable_registration: true
|
enable_registration: true
|
||||||
|
allow_numeric_ids: true
|
||||||
|
numeric_ids_prefix: "numericuser"
|
||||||
```
|
```
|
||||||
---
|
---
|
||||||
### `sso`
|
### `sso`
|
||||||
|
@ -4546,3 +4595,32 @@ background_updates:
|
||||||
min_batch_size: 10
|
min_batch_size: 10
|
||||||
default_batch_size: 50
|
default_batch_size: 50
|
||||||
```
|
```
|
||||||
|
---
|
||||||
|
## Auto Accept Invites
|
||||||
|
Configuration settings related to automatically accepting invites.
|
||||||
|
|
||||||
|
---
|
||||||
|
### `auto_accept_invites`
|
||||||
|
|
||||||
|
Automatically accepting invites controls whether users are presented with an invite request or if they
|
||||||
|
are instead automatically joined to a room when receiving an invite. Set the `enabled` sub-option to true to
|
||||||
|
enable auto-accepting invites. Defaults to false.
|
||||||
|
This setting has the following sub-options:
|
||||||
|
* `enabled`: Whether to run the auto-accept invites logic. Defaults to false.
|
||||||
|
* `only_for_direct_messages`: Whether invites should be automatically accepted for all room types, or only
|
||||||
|
for direct messages. Defaults to false.
|
||||||
|
* `only_from_local_users`: Whether to only automatically accept invites from users on this homeserver. Defaults to false.
|
||||||
|
* `worker_to_run_on`: Which worker to run this module on. This must match the "worker_name".
|
||||||
|
|
||||||
|
NOTE: Care should be taken not to enable this setting if the `synapse_auto_accept_invite` module is enabled and installed.
|
||||||
|
The two modules will compete to perform the same task and may result in undesired behaviour. For example, multiple join
|
||||||
|
events could be generated from a single invite.
|
||||||
|
|
||||||
|
Example configuration:
|
||||||
|
```yaml
|
||||||
|
auto_accept_invites:
|
||||||
|
enabled: true
|
||||||
|
only_for_direct_messages: true
|
||||||
|
only_from_local_users: true
|
||||||
|
worker_to_run_on: "worker_1"
|
||||||
|
```
|
||||||
|
|
|
@ -86,9 +86,9 @@ The search term is then split into words:
|
||||||
* If unavailable, then runs of ASCII characters, numbers, underscores, and hyphens
|
* If unavailable, then runs of ASCII characters, numbers, underscores, and hyphens
|
||||||
are considered words.
|
are considered words.
|
||||||
|
|
||||||
The queries for PostgreSQL and SQLite are detailed below, by their overall goal
|
The queries for PostgreSQL and SQLite are detailed below, but their overall goal
|
||||||
is to find matching users, preferring users who are "real" (e.g. not bots,
|
is to find matching users, preferring users who are "real" (e.g. not bots,
|
||||||
not deactivated). It is assumed that real users will have an display name and
|
not deactivated). It is assumed that real users will have a display name and
|
||||||
avatar set.
|
avatar set.
|
||||||
|
|
||||||
### PostgreSQL
|
### PostgreSQL
|
||||||
|
|
|
@ -211,6 +211,8 @@ information.
|
||||||
^/_matrix/federation/v1/make_leave/
|
^/_matrix/federation/v1/make_leave/
|
||||||
^/_matrix/federation/(v1|v2)/send_join/
|
^/_matrix/federation/(v1|v2)/send_join/
|
||||||
^/_matrix/federation/(v1|v2)/send_leave/
|
^/_matrix/federation/(v1|v2)/send_leave/
|
||||||
|
^/_matrix/federation/v1/make_knock/
|
||||||
|
^/_matrix/federation/v1/send_knock/
|
||||||
^/_matrix/federation/(v1|v2)/invite/
|
^/_matrix/federation/(v1|v2)/invite/
|
||||||
^/_matrix/federation/v1/event_auth/
|
^/_matrix/federation/v1/event_auth/
|
||||||
^/_matrix/federation/v1/timestamp_to_event/
|
^/_matrix/federation/v1/timestamp_to_event/
|
||||||
|
@ -232,7 +234,7 @@ information.
|
||||||
^/_matrix/client/v1/rooms/.*/hierarchy$
|
^/_matrix/client/v1/rooms/.*/hierarchy$
|
||||||
^/_matrix/client/(v1|unstable)/rooms/.*/relations/
|
^/_matrix/client/(v1|unstable)/rooms/.*/relations/
|
||||||
^/_matrix/client/v1/rooms/.*/threads$
|
^/_matrix/client/v1/rooms/.*/threads$
|
||||||
^/_matrix/client/unstable/im.nheko.summary/rooms/.*/summary$
|
^/_matrix/client/unstable/im.nheko.summary/summary/.*$
|
||||||
^/_matrix/client/(r0|v3|unstable)/account/3pid$
|
^/_matrix/client/(r0|v3|unstable)/account/3pid$
|
||||||
^/_matrix/client/(r0|v3|unstable)/account/whoami$
|
^/_matrix/client/(r0|v3|unstable)/account/whoami$
|
||||||
^/_matrix/client/(r0|v3|unstable)/devices$
|
^/_matrix/client/(r0|v3|unstable)/devices$
|
||||||
|
@ -535,7 +537,7 @@ the stream writer for the `presence` stream:
|
||||||
##### The `push_rules` stream
|
##### The `push_rules` stream
|
||||||
|
|
||||||
The following endpoints should be routed directly to the worker configured as
|
The following endpoints should be routed directly to the worker configured as
|
||||||
the stream writer for the `push` stream:
|
the stream writer for the `push_rules` stream:
|
||||||
|
|
||||||
^/_matrix/client/(api/v1|r0|v3|unstable)/pushrules/
|
^/_matrix/client/(api/v1|r0|v3|unstable)/pushrules/
|
||||||
|
|
||||||
|
@ -634,7 +636,7 @@ worker application type.
|
||||||
|
|
||||||
#### Push Notifications
|
#### Push Notifications
|
||||||
|
|
||||||
You can designate generic worker to sending push notifications to
|
You can designate generic workers to send push notifications to
|
||||||
a [push gateway](https://spec.matrix.org/v1.5/push-gateway-api/) such as
|
a [push gateway](https://spec.matrix.org/v1.5/push-gateway-api/) such as
|
||||||
[sygnal](https://github.com/matrix-org/sygnal) and email.
|
[sygnal](https://github.com/matrix-org/sygnal) and email.
|
||||||
|
|
||||||
|
|
1308
poetry.lock
generated
1308
poetry.lock
generated
File diff suppressed because it is too large
Load diff
|
@ -96,7 +96,7 @@ module-name = "synapse.synapse_rust"
|
||||||
|
|
||||||
[tool.poetry]
|
[tool.poetry]
|
||||||
name = "matrix-synapse"
|
name = "matrix-synapse"
|
||||||
version = "1.105.1"
|
version = "1.109.0rc1"
|
||||||
description = "Homeserver for the Matrix decentralised comms protocol"
|
description = "Homeserver for the Matrix decentralised comms protocol"
|
||||||
authors = ["Matrix.org Team and Contributors <packages@matrix.org>"]
|
authors = ["Matrix.org Team and Contributors <packages@matrix.org>"]
|
||||||
license = "AGPL-3.0-or-later"
|
license = "AGPL-3.0-or-later"
|
||||||
|
@ -200,10 +200,8 @@ netaddr = ">=0.7.18"
|
||||||
# add a lower bound to the Jinja2 dependency.
|
# add a lower bound to the Jinja2 dependency.
|
||||||
Jinja2 = ">=3.0"
|
Jinja2 = ">=3.0"
|
||||||
bleach = ">=1.4.3"
|
bleach = ">=1.4.3"
|
||||||
# We use `ParamSpec` and `Concatenate`, which were added in `typing-extensions` 3.10.0.0.
|
# We use `Self`, which were added in `typing-extensions` 4.0.
|
||||||
# Additionally we need https://github.com/python/typing/pull/817 to allow types to be
|
typing-extensions = ">=4.0"
|
||||||
# generic over ParamSpecs.
|
|
||||||
typing-extensions = ">=3.10.0.1"
|
|
||||||
# We enforce that we have a `cryptography` version that bundles an `openssl`
|
# We enforce that we have a `cryptography` version that bundles an `openssl`
|
||||||
# with the latest security patches.
|
# with the latest security patches.
|
||||||
cryptography = ">=3.4.7"
|
cryptography = ">=3.4.7"
|
||||||
|
@ -321,7 +319,7 @@ all = [
|
||||||
# This helps prevents merge conflicts when running a batch of dependabot updates.
|
# This helps prevents merge conflicts when running a batch of dependabot updates.
|
||||||
isort = ">=5.10.1"
|
isort = ">=5.10.1"
|
||||||
black = ">=22.7.0"
|
black = ">=22.7.0"
|
||||||
ruff = "0.3.5"
|
ruff = "0.3.7"
|
||||||
# Type checking only works with the pydantic.v1 compat module from pydantic v2
|
# Type checking only works with the pydantic.v1 compat module from pydantic v2
|
||||||
pydantic = "^2"
|
pydantic = "^2"
|
||||||
|
|
||||||
|
@ -364,17 +362,6 @@ towncrier = ">=18.6.0rc1"
|
||||||
tomli = ">=1.2.3"
|
tomli = ">=1.2.3"
|
||||||
|
|
||||||
|
|
||||||
# Dependencies for building the development documentation
|
|
||||||
[tool.poetry.group.dev-docs]
|
|
||||||
optional = true
|
|
||||||
|
|
||||||
[tool.poetry.group.dev-docs.dependencies]
|
|
||||||
sphinx = {version = "^6.1", python = "^3.8"}
|
|
||||||
sphinx-autodoc2 = {version = ">=0.4.2,<0.6.0", python = "^3.8"}
|
|
||||||
myst-parser = {version = "^1.0.0", python = "^3.8"}
|
|
||||||
furo = ">=2022.12.7,<2025.0.0"
|
|
||||||
|
|
||||||
|
|
||||||
[build-system]
|
[build-system]
|
||||||
# The upper bounds here are defensive, intended to prevent situations like
|
# The upper bounds here are defensive, intended to prevent situations like
|
||||||
# https://github.com/matrix-org/synapse/issues/13849 and
|
# https://github.com/matrix-org/synapse/issues/13849 and
|
||||||
|
|
1170
requirements.txt
Normal file
1170
requirements.txt
Normal file
File diff suppressed because it is too large
Load diff
|
@ -7,7 +7,7 @@ name = "synapse"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
|
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
rust-version = "1.65.0"
|
rust-version = "1.66.0"
|
||||||
|
|
||||||
[lib]
|
[lib]
|
||||||
name = "synapse"
|
name = "synapse"
|
||||||
|
@ -23,19 +23,26 @@ name = "synapse.synapse_rust"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
anyhow = "1.0.63"
|
anyhow = "1.0.63"
|
||||||
|
base64 = "0.21.7"
|
||||||
|
bytes = "1.6.0"
|
||||||
|
headers = "0.4.0"
|
||||||
|
http = "1.1.0"
|
||||||
lazy_static = "1.4.0"
|
lazy_static = "1.4.0"
|
||||||
log = "0.4.17"
|
log = "0.4.17"
|
||||||
pyo3 = { version = "0.20.0", features = [
|
mime = "0.3.17"
|
||||||
|
pyo3 = { version = "0.21.0", features = [
|
||||||
"macros",
|
"macros",
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"abi3",
|
"abi3",
|
||||||
"abi3-py38",
|
"abi3-py38",
|
||||||
] }
|
] }
|
||||||
pyo3-log = "0.9.0"
|
pyo3-log = "0.10.0"
|
||||||
pythonize = "0.20.0"
|
pythonize = "0.21.0"
|
||||||
regex = "1.6.0"
|
regex = "1.6.0"
|
||||||
|
sha2 = "0.10.8"
|
||||||
serde = { version = "1.0.144", features = ["derive"] }
|
serde = { version = "1.0.144", features = ["derive"] }
|
||||||
serde_json = "1.0.85"
|
serde_json = "1.0.85"
|
||||||
|
ulid = "1.1.2"
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
extension-module = ["pyo3/extension-module"]
|
extension-module = ["pyo3/extension-module"]
|
||||||
|
|
|
@ -25,21 +25,21 @@ use std::net::Ipv4Addr;
|
||||||
use std::str::FromStr;
|
use std::str::FromStr;
|
||||||
|
|
||||||
use anyhow::Error;
|
use anyhow::Error;
|
||||||
use pyo3::prelude::*;
|
use pyo3::{prelude::*, pybacked::PyBackedStr};
|
||||||
use regex::Regex;
|
use regex::Regex;
|
||||||
|
|
||||||
use crate::push::utils::{glob_to_regex, GlobMatchType};
|
use crate::push::utils::{glob_to_regex, GlobMatchType};
|
||||||
|
|
||||||
/// Called when registering modules with python.
|
/// Called when registering modules with python.
|
||||||
pub fn register_module(py: Python<'_>, m: &PyModule) -> PyResult<()> {
|
pub fn register_module(py: Python<'_>, m: &Bound<'_, PyModule>) -> PyResult<()> {
|
||||||
let child_module = PyModule::new(py, "acl")?;
|
let child_module = PyModule::new_bound(py, "acl")?;
|
||||||
child_module.add_class::<ServerAclEvaluator>()?;
|
child_module.add_class::<ServerAclEvaluator>()?;
|
||||||
|
|
||||||
m.add_submodule(child_module)?;
|
m.add_submodule(&child_module)?;
|
||||||
|
|
||||||
// We need to manually add the module to sys.modules to make `from
|
// We need to manually add the module to sys.modules to make `from
|
||||||
// synapse.synapse_rust import acl` work.
|
// synapse.synapse_rust import acl` work.
|
||||||
py.import("sys")?
|
py.import_bound("sys")?
|
||||||
.getattr("modules")?
|
.getattr("modules")?
|
||||||
.set_item("synapse.synapse_rust.acl", child_module)?;
|
.set_item("synapse.synapse_rust.acl", child_module)?;
|
||||||
|
|
||||||
|
@ -59,8 +59,8 @@ impl ServerAclEvaluator {
|
||||||
#[new]
|
#[new]
|
||||||
pub fn py_new(
|
pub fn py_new(
|
||||||
allow_ip_literals: bool,
|
allow_ip_literals: bool,
|
||||||
allow: Vec<&str>,
|
allow: Vec<PyBackedStr>,
|
||||||
deny: Vec<&str>,
|
deny: Vec<PyBackedStr>,
|
||||||
) -> Result<Self, Error> {
|
) -> Result<Self, Error> {
|
||||||
let allow = allow
|
let allow = allow
|
||||||
.iter()
|
.iter()
|
||||||
|
|
60
rust/src/errors.rs
Normal file
60
rust/src/errors.rs
Normal file
|
@ -0,0 +1,60 @@
|
||||||
|
/*
|
||||||
|
* This file is licensed under the Affero General Public License (AGPL) version 3.
|
||||||
|
*
|
||||||
|
* Copyright (C) 2024 New Vector, Ltd
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU Affero General Public License as
|
||||||
|
* published by the Free Software Foundation, either version 3 of the
|
||||||
|
* License, or (at your option) any later version.
|
||||||
|
*
|
||||||
|
* See the GNU Affero General Public License for more details:
|
||||||
|
* <https://www.gnu.org/licenses/agpl-3.0.html>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#![allow(clippy::new_ret_no_self)]
|
||||||
|
|
||||||
|
use std::collections::HashMap;
|
||||||
|
|
||||||
|
use http::{HeaderMap, StatusCode};
|
||||||
|
use pyo3::{exceptions::PyValueError, import_exception};
|
||||||
|
|
||||||
|
import_exception!(synapse.api.errors, SynapseError);
|
||||||
|
|
||||||
|
impl SynapseError {
|
||||||
|
pub fn new(
|
||||||
|
code: StatusCode,
|
||||||
|
message: String,
|
||||||
|
errcode: &'static str,
|
||||||
|
additional_fields: Option<HashMap<String, String>>,
|
||||||
|
headers: Option<HeaderMap>,
|
||||||
|
) -> pyo3::PyErr {
|
||||||
|
// Transform the HeaderMap into a HashMap<String, String>
|
||||||
|
let headers = if let Some(headers) = headers {
|
||||||
|
let mut map = HashMap::with_capacity(headers.len());
|
||||||
|
for (key, value) in headers.iter() {
|
||||||
|
let Ok(value) = value.to_str() else {
|
||||||
|
// This should never happen, but we don't want to panic in case it does
|
||||||
|
return PyValueError::new_err(
|
||||||
|
"Could not construct SynapseError: header value is not valid ASCII",
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
map.insert(key.as_str().to_owned(), value.to_owned());
|
||||||
|
}
|
||||||
|
Some(map)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
|
|
||||||
|
SynapseError::new_err((code.as_u16(), message, errcode, additional_fields, headers))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
import_exception!(synapse.api.errors, NotFoundError);
|
||||||
|
|
||||||
|
impl NotFoundError {
|
||||||
|
pub fn new() -> pyo3::PyErr {
|
||||||
|
NotFoundError::new_err(())
|
||||||
|
}
|
||||||
|
}
|
|
@ -20,8 +20,10 @@
|
||||||
|
|
||||||
//! Implements the internal metadata class attached to events.
|
//! Implements the internal metadata class attached to events.
|
||||||
//!
|
//!
|
||||||
//! The internal metadata is a bit like a `TypedDict`, in that it is stored as a
|
//! The internal metadata is a bit like a `TypedDict`, in that most of
|
||||||
//! JSON dict in the DB. Most events have zero, or only a few, of these keys
|
//! it is stored as a JSON dict in the DB (the exceptions being `outlier`
|
||||||
|
//! and `stream_ordering` which have their own columns in the database).
|
||||||
|
//! Most events have zero, or only a few, of these keys
|
||||||
//! set. Therefore, since we care more about memory size than performance here,
|
//! set. Therefore, since we care more about memory size than performance here,
|
||||||
//! we store these fields in a mapping.
|
//! we store these fields in a mapping.
|
||||||
//!
|
//!
|
||||||
|
@ -36,9 +38,10 @@ use anyhow::Context;
|
||||||
use log::warn;
|
use log::warn;
|
||||||
use pyo3::{
|
use pyo3::{
|
||||||
exceptions::PyAttributeError,
|
exceptions::PyAttributeError,
|
||||||
|
pybacked::PyBackedStr,
|
||||||
pyclass, pymethods,
|
pyclass, pymethods,
|
||||||
types::{PyDict, PyString},
|
types::{PyAnyMethods, PyDict, PyDictMethods, PyString},
|
||||||
IntoPy, PyAny, PyObject, PyResult, Python,
|
Bound, IntoPy, PyAny, PyObject, PyResult, Python,
|
||||||
};
|
};
|
||||||
|
|
||||||
/// Definitions of the various fields of the internal metadata.
|
/// Definitions of the various fields of the internal metadata.
|
||||||
|
@ -57,7 +60,7 @@ enum EventInternalMetadataData {
|
||||||
|
|
||||||
impl EventInternalMetadataData {
|
impl EventInternalMetadataData {
|
||||||
/// Convert the field to its name and python object.
|
/// Convert the field to its name and python object.
|
||||||
fn to_python_pair<'a>(&self, py: Python<'a>) -> (&'a PyString, PyObject) {
|
fn to_python_pair<'a>(&self, py: Python<'a>) -> (&'a Bound<'a, PyString>, PyObject) {
|
||||||
match self {
|
match self {
|
||||||
EventInternalMetadataData::OutOfBandMembership(o) => {
|
EventInternalMetadataData::OutOfBandMembership(o) => {
|
||||||
(pyo3::intern!(py, "out_of_band_membership"), o.into_py(py))
|
(pyo3::intern!(py, "out_of_band_membership"), o.into_py(py))
|
||||||
|
@ -88,10 +91,13 @@ impl EventInternalMetadataData {
|
||||||
/// Converts from python key/values to the field.
|
/// Converts from python key/values to the field.
|
||||||
///
|
///
|
||||||
/// Returns `None` if the key is a valid but unrecognized string.
|
/// Returns `None` if the key is a valid but unrecognized string.
|
||||||
fn from_python_pair(key: &PyAny, value: &PyAny) -> PyResult<Option<Self>> {
|
fn from_python_pair(
|
||||||
let key_str: &str = key.extract()?;
|
key: &Bound<'_, PyAny>,
|
||||||
|
value: &Bound<'_, PyAny>,
|
||||||
|
) -> PyResult<Option<Self>> {
|
||||||
|
let key_str: PyBackedStr = key.extract()?;
|
||||||
|
|
||||||
let e = match key_str {
|
let e = match &*key_str {
|
||||||
"out_of_band_membership" => EventInternalMetadataData::OutOfBandMembership(
|
"out_of_band_membership" => EventInternalMetadataData::OutOfBandMembership(
|
||||||
value
|
value
|
||||||
.extract()
|
.extract()
|
||||||
|
@ -208,11 +214,11 @@ pub struct EventInternalMetadata {
|
||||||
#[pymethods]
|
#[pymethods]
|
||||||
impl EventInternalMetadata {
|
impl EventInternalMetadata {
|
||||||
#[new]
|
#[new]
|
||||||
fn new(dict: &PyDict) -> PyResult<Self> {
|
fn new(dict: &Bound<'_, PyDict>) -> PyResult<Self> {
|
||||||
let mut data = Vec::with_capacity(dict.len());
|
let mut data = Vec::with_capacity(dict.len());
|
||||||
|
|
||||||
for (key, value) in dict.iter() {
|
for (key, value) in dict.iter() {
|
||||||
match EventInternalMetadataData::from_python_pair(key, value) {
|
match EventInternalMetadataData::from_python_pair(&key, &value) {
|
||||||
Ok(Some(entry)) => data.push(entry),
|
Ok(Some(entry)) => data.push(entry),
|
||||||
Ok(None) => {}
|
Ok(None) => {}
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
|
@ -234,8 +240,11 @@ impl EventInternalMetadata {
|
||||||
self.clone()
|
self.clone()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Get a dict holding the data stored in the `internal_metadata` column in the database.
|
||||||
|
///
|
||||||
|
/// Note that `outlier` and `stream_ordering` are stored in separate columns so are not returned here.
|
||||||
fn get_dict(&self, py: Python<'_>) -> PyResult<PyObject> {
|
fn get_dict(&self, py: Python<'_>) -> PyResult<PyObject> {
|
||||||
let dict = PyDict::new(py);
|
let dict = PyDict::new_bound(py);
|
||||||
|
|
||||||
for entry in &self.data {
|
for entry in &self.data {
|
||||||
let (key, value) = entry.to_python_pair(py);
|
let (key, value) = entry.to_python_pair(py);
|
||||||
|
|
|
@ -20,20 +20,23 @@
|
||||||
|
|
||||||
//! Classes for representing Events.
|
//! Classes for representing Events.
|
||||||
|
|
||||||
use pyo3::{types::PyModule, PyResult, Python};
|
use pyo3::{
|
||||||
|
types::{PyAnyMethods, PyModule, PyModuleMethods},
|
||||||
|
Bound, PyResult, Python,
|
||||||
|
};
|
||||||
|
|
||||||
mod internal_metadata;
|
mod internal_metadata;
|
||||||
|
|
||||||
/// Called when registering modules with python.
|
/// Called when registering modules with python.
|
||||||
pub fn register_module(py: Python<'_>, m: &PyModule) -> PyResult<()> {
|
pub fn register_module(py: Python<'_>, m: &Bound<'_, PyModule>) -> PyResult<()> {
|
||||||
let child_module = PyModule::new(py, "events")?;
|
let child_module = PyModule::new_bound(py, "events")?;
|
||||||
child_module.add_class::<internal_metadata::EventInternalMetadata>()?;
|
child_module.add_class::<internal_metadata::EventInternalMetadata>()?;
|
||||||
|
|
||||||
m.add_submodule(child_module)?;
|
m.add_submodule(&child_module)?;
|
||||||
|
|
||||||
// We need to manually add the module to sys.modules to make `from
|
// We need to manually add the module to sys.modules to make `from
|
||||||
// synapse.synapse_rust import events` work.
|
// synapse.synapse_rust import events` work.
|
||||||
py.import("sys")?
|
py.import_bound("sys")?
|
||||||
.getattr("modules")?
|
.getattr("modules")?
|
||||||
.set_item("synapse.synapse_rust.events", child_module)?;
|
.set_item("synapse.synapse_rust.events", child_module)?;
|
||||||
|
|
||||||
|
|
174
rust/src/http.rs
Normal file
174
rust/src/http.rs
Normal file
|
@ -0,0 +1,174 @@
|
||||||
|
/*
|
||||||
|
* This file is licensed under the Affero General Public License (AGPL) version 3.
|
||||||
|
*
|
||||||
|
* Copyright (C) 2024 New Vector, Ltd
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU Affero General Public License as
|
||||||
|
* published by the Free Software Foundation, either version 3 of the
|
||||||
|
* License, or (at your option) any later version.
|
||||||
|
*
|
||||||
|
* See the GNU Affero General Public License for more details:
|
||||||
|
* <https://www.gnu.org/licenses/agpl-3.0.html>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
use bytes::{Buf, BufMut, Bytes, BytesMut};
|
||||||
|
use headers::{Header, HeaderMapExt};
|
||||||
|
use http::{HeaderName, HeaderValue, Method, Request, Response, StatusCode, Uri};
|
||||||
|
use pyo3::{
|
||||||
|
exceptions::PyValueError,
|
||||||
|
types::{PyAnyMethods, PyBytes, PyBytesMethods, PySequence, PyTuple},
|
||||||
|
Bound, PyAny, PyResult,
|
||||||
|
};
|
||||||
|
|
||||||
|
use crate::errors::SynapseError;
|
||||||
|
|
||||||
|
/// Read a file-like Python object by chunks
|
||||||
|
///
|
||||||
|
/// # Errors
|
||||||
|
///
|
||||||
|
/// Returns an error if calling the `read` on the Python object failed
|
||||||
|
fn read_io_body(body: &Bound<'_, PyAny>, chunk_size: usize) -> PyResult<Bytes> {
|
||||||
|
let mut buf = BytesMut::new();
|
||||||
|
loop {
|
||||||
|
let bound = &body.call_method1("read", (chunk_size,))?;
|
||||||
|
let bytes: &Bound<'_, PyBytes> = bound.downcast()?;
|
||||||
|
if bytes.as_bytes().is_empty() {
|
||||||
|
return Ok(buf.into());
|
||||||
|
}
|
||||||
|
buf.put(bytes.as_bytes());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Transform a Twisted `IRequest` to an [`http::Request`]
|
||||||
|
///
|
||||||
|
/// It uses the following members of `IRequest`:
|
||||||
|
/// - `content`, which is expected to be a file-like object with a `read` method
|
||||||
|
/// - `uri`, which is expected to be a valid URI as `bytes`
|
||||||
|
/// - `method`, which is expected to be a valid HTTP method as `bytes`
|
||||||
|
/// - `requestHeaders`, which is expected to have a `getAllRawHeaders` method
|
||||||
|
///
|
||||||
|
/// # Errors
|
||||||
|
///
|
||||||
|
/// Returns an error if the Python object doesn't properly implement `IRequest`
|
||||||
|
pub fn http_request_from_twisted(request: &Bound<'_, PyAny>) -> PyResult<Request<Bytes>> {
|
||||||
|
let content = request.getattr("content")?;
|
||||||
|
let body = read_io_body(&content, 4096)?;
|
||||||
|
|
||||||
|
let mut req = Request::new(body);
|
||||||
|
|
||||||
|
let bound = &request.getattr("uri")?;
|
||||||
|
let uri: &Bound<'_, PyBytes> = bound.downcast()?;
|
||||||
|
*req.uri_mut() =
|
||||||
|
Uri::try_from(uri.as_bytes()).map_err(|_| PyValueError::new_err("invalid uri"))?;
|
||||||
|
|
||||||
|
let bound = &request.getattr("method")?;
|
||||||
|
let method: &Bound<'_, PyBytes> = bound.downcast()?;
|
||||||
|
*req.method_mut() = Method::from_bytes(method.as_bytes())
|
||||||
|
.map_err(|_| PyValueError::new_err("invalid method"))?;
|
||||||
|
|
||||||
|
let headers_iter = request
|
||||||
|
.getattr("requestHeaders")?
|
||||||
|
.call_method0("getAllRawHeaders")?
|
||||||
|
.iter()?;
|
||||||
|
|
||||||
|
for header in headers_iter {
|
||||||
|
let header = header?;
|
||||||
|
let header: &Bound<'_, PyTuple> = header.downcast()?;
|
||||||
|
let bound = &header.get_item(0)?;
|
||||||
|
let name: &Bound<'_, PyBytes> = bound.downcast()?;
|
||||||
|
let name = HeaderName::from_bytes(name.as_bytes())
|
||||||
|
.map_err(|_| PyValueError::new_err("invalid header name"))?;
|
||||||
|
|
||||||
|
let bound = &header.get_item(1)?;
|
||||||
|
let values: &Bound<'_, PySequence> = bound.downcast()?;
|
||||||
|
for index in 0..values.len()? {
|
||||||
|
let bound = &values.get_item(index)?;
|
||||||
|
let value: &Bound<'_, PyBytes> = bound.downcast()?;
|
||||||
|
let value = HeaderValue::from_bytes(value.as_bytes())
|
||||||
|
.map_err(|_| PyValueError::new_err("invalid header value"))?;
|
||||||
|
req.headers_mut().append(name.clone(), value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(req)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Send an [`http::Response`] through a Twisted `IRequest`
|
||||||
|
///
|
||||||
|
/// It uses the following members of `IRequest`:
|
||||||
|
///
|
||||||
|
/// - `responseHeaders`, which is expected to have a `addRawHeader(bytes, bytes)` method
|
||||||
|
/// - `setResponseCode(int)` method
|
||||||
|
/// - `write(bytes)` method
|
||||||
|
/// - `finish()` method
|
||||||
|
///
|
||||||
|
/// # Errors
|
||||||
|
///
|
||||||
|
/// Returns an error if the Python object doesn't properly implement `IRequest`
|
||||||
|
pub fn http_response_to_twisted<B>(
|
||||||
|
request: &Bound<'_, PyAny>,
|
||||||
|
response: Response<B>,
|
||||||
|
) -> PyResult<()>
|
||||||
|
where
|
||||||
|
B: Buf,
|
||||||
|
{
|
||||||
|
let (parts, mut body) = response.into_parts();
|
||||||
|
|
||||||
|
request.call_method1("setResponseCode", (parts.status.as_u16(),))?;
|
||||||
|
|
||||||
|
let response_headers = request.getattr("responseHeaders")?;
|
||||||
|
for (name, value) in parts.headers.iter() {
|
||||||
|
response_headers.call_method1("addRawHeader", (name.as_str(), value.as_bytes()))?;
|
||||||
|
}
|
||||||
|
|
||||||
|
while body.remaining() != 0 {
|
||||||
|
let chunk = body.chunk();
|
||||||
|
request.call_method1("write", (chunk,))?;
|
||||||
|
body.advance(chunk.len());
|
||||||
|
}
|
||||||
|
|
||||||
|
request.call_method0("finish")?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// An extension trait for [`HeaderMap`] that provides typed access to headers, and throws the
|
||||||
|
/// right python exceptions when the header is missing or fails to parse.
|
||||||
|
///
|
||||||
|
/// [`HeaderMap`]: headers::HeaderMap
|
||||||
|
pub trait HeaderMapPyExt: HeaderMapExt {
|
||||||
|
/// Get a header from the map, returning an error if it is missing or invalid.
|
||||||
|
fn typed_get_required<H>(&self) -> PyResult<H>
|
||||||
|
where
|
||||||
|
H: Header,
|
||||||
|
{
|
||||||
|
self.typed_get_optional::<H>()?.ok_or_else(|| {
|
||||||
|
SynapseError::new(
|
||||||
|
StatusCode::BAD_REQUEST,
|
||||||
|
format!("Missing required header: {}", H::name()),
|
||||||
|
"M_MISSING_PARAM",
|
||||||
|
None,
|
||||||
|
None,
|
||||||
|
)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get a header from the map, returning `None` if it is missing and an error if it is invalid.
|
||||||
|
fn typed_get_optional<H>(&self) -> PyResult<Option<H>>
|
||||||
|
where
|
||||||
|
H: Header,
|
||||||
|
{
|
||||||
|
self.typed_try_get::<H>().map_err(|_| {
|
||||||
|
SynapseError::new(
|
||||||
|
StatusCode::BAD_REQUEST,
|
||||||
|
format!("Invalid header: {}", H::name()),
|
||||||
|
"M_INVALID_PARAM",
|
||||||
|
None,
|
||||||
|
None,
|
||||||
|
)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T: HeaderMapExt> HeaderMapPyExt for T {}
|
|
@ -3,8 +3,11 @@ use pyo3::prelude::*;
|
||||||
use pyo3_log::ResetHandle;
|
use pyo3_log::ResetHandle;
|
||||||
|
|
||||||
pub mod acl;
|
pub mod acl;
|
||||||
|
pub mod errors;
|
||||||
pub mod events;
|
pub mod events;
|
||||||
|
pub mod http;
|
||||||
pub mod push;
|
pub mod push;
|
||||||
|
pub mod rendezvous;
|
||||||
|
|
||||||
lazy_static! {
|
lazy_static! {
|
||||||
static ref LOGGING_HANDLE: ResetHandle = pyo3_log::init();
|
static ref LOGGING_HANDLE: ResetHandle = pyo3_log::init();
|
||||||
|
@ -35,7 +38,7 @@ fn reset_logging_config() {
|
||||||
|
|
||||||
/// The entry point for defining the Python module.
|
/// The entry point for defining the Python module.
|
||||||
#[pymodule]
|
#[pymodule]
|
||||||
fn synapse_rust(py: Python<'_>, m: &PyModule) -> PyResult<()> {
|
fn synapse_rust(py: Python<'_>, m: &Bound<'_, PyModule>) -> PyResult<()> {
|
||||||
m.add_function(wrap_pyfunction!(sum_as_string, m)?)?;
|
m.add_function(wrap_pyfunction!(sum_as_string, m)?)?;
|
||||||
m.add_function(wrap_pyfunction!(get_rust_file_digest, m)?)?;
|
m.add_function(wrap_pyfunction!(get_rust_file_digest, m)?)?;
|
||||||
m.add_function(wrap_pyfunction!(reset_logging_config, m)?)?;
|
m.add_function(wrap_pyfunction!(reset_logging_config, m)?)?;
|
||||||
|
@ -43,6 +46,7 @@ fn synapse_rust(py: Python<'_>, m: &PyModule) -> PyResult<()> {
|
||||||
acl::register_module(py, m)?;
|
acl::register_module(py, m)?;
|
||||||
push::register_module(py, m)?;
|
push::register_module(py, m)?;
|
||||||
events::register_module(py, m)?;
|
events::register_module(py, m)?;
|
||||||
|
rendezvous::register_module(py, m)?;
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
|
@ -66,7 +66,7 @@ use log::warn;
|
||||||
use pyo3::exceptions::PyTypeError;
|
use pyo3::exceptions::PyTypeError;
|
||||||
use pyo3::prelude::*;
|
use pyo3::prelude::*;
|
||||||
use pyo3::types::{PyBool, PyList, PyLong, PyString};
|
use pyo3::types::{PyBool, PyList, PyLong, PyString};
|
||||||
use pythonize::{depythonize, pythonize};
|
use pythonize::{depythonize_bound, pythonize};
|
||||||
use serde::de::Error as _;
|
use serde::de::Error as _;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use serde_json::Value;
|
use serde_json::Value;
|
||||||
|
@ -78,19 +78,19 @@ pub mod evaluator;
|
||||||
pub mod utils;
|
pub mod utils;
|
||||||
|
|
||||||
/// Called when registering modules with python.
|
/// Called when registering modules with python.
|
||||||
pub fn register_module(py: Python<'_>, m: &PyModule) -> PyResult<()> {
|
pub fn register_module(py: Python<'_>, m: &Bound<'_, PyModule>) -> PyResult<()> {
|
||||||
let child_module = PyModule::new(py, "push")?;
|
let child_module = PyModule::new_bound(py, "push")?;
|
||||||
child_module.add_class::<PushRule>()?;
|
child_module.add_class::<PushRule>()?;
|
||||||
child_module.add_class::<PushRules>()?;
|
child_module.add_class::<PushRules>()?;
|
||||||
child_module.add_class::<FilteredPushRules>()?;
|
child_module.add_class::<FilteredPushRules>()?;
|
||||||
child_module.add_class::<PushRuleEvaluator>()?;
|
child_module.add_class::<PushRuleEvaluator>()?;
|
||||||
child_module.add_function(wrap_pyfunction!(get_base_rule_ids, m)?)?;
|
child_module.add_function(wrap_pyfunction!(get_base_rule_ids, m)?)?;
|
||||||
|
|
||||||
m.add_submodule(child_module)?;
|
m.add_submodule(&child_module)?;
|
||||||
|
|
||||||
// We need to manually add the module to sys.modules to make `from
|
// We need to manually add the module to sys.modules to make `from
|
||||||
// synapse.synapse_rust import push` work.
|
// synapse.synapse_rust import push` work.
|
||||||
py.import("sys")?
|
py.import_bound("sys")?
|
||||||
.getattr("modules")?
|
.getattr("modules")?
|
||||||
.set_item("synapse.synapse_rust.push", child_module)?;
|
.set_item("synapse.synapse_rust.push", child_module)?;
|
||||||
|
|
||||||
|
@ -271,12 +271,12 @@ pub enum SimpleJsonValue {
|
||||||
|
|
||||||
impl<'source> FromPyObject<'source> for SimpleJsonValue {
|
impl<'source> FromPyObject<'source> for SimpleJsonValue {
|
||||||
fn extract(ob: &'source PyAny) -> PyResult<Self> {
|
fn extract(ob: &'source PyAny) -> PyResult<Self> {
|
||||||
if let Ok(s) = <PyString as pyo3::PyTryFrom>::try_from(ob) {
|
if let Ok(s) = ob.downcast::<PyString>() {
|
||||||
Ok(SimpleJsonValue::Str(Cow::Owned(s.to_string())))
|
Ok(SimpleJsonValue::Str(Cow::Owned(s.to_string())))
|
||||||
// A bool *is* an int, ensure we try bool first.
|
// A bool *is* an int, ensure we try bool first.
|
||||||
} else if let Ok(b) = <PyBool as pyo3::PyTryFrom>::try_from(ob) {
|
} else if let Ok(b) = ob.downcast::<PyBool>() {
|
||||||
Ok(SimpleJsonValue::Bool(b.extract()?))
|
Ok(SimpleJsonValue::Bool(b.extract()?))
|
||||||
} else if let Ok(i) = <PyLong as pyo3::PyTryFrom>::try_from(ob) {
|
} else if let Ok(i) = ob.downcast::<PyLong>() {
|
||||||
Ok(SimpleJsonValue::Int(i.extract()?))
|
Ok(SimpleJsonValue::Int(i.extract()?))
|
||||||
} else if ob.is_none() {
|
} else if ob.is_none() {
|
||||||
Ok(SimpleJsonValue::Null)
|
Ok(SimpleJsonValue::Null)
|
||||||
|
@ -299,7 +299,7 @@ pub enum JsonValue {
|
||||||
|
|
||||||
impl<'source> FromPyObject<'source> for JsonValue {
|
impl<'source> FromPyObject<'source> for JsonValue {
|
||||||
fn extract(ob: &'source PyAny) -> PyResult<Self> {
|
fn extract(ob: &'source PyAny) -> PyResult<Self> {
|
||||||
if let Ok(l) = <PyList as pyo3::PyTryFrom>::try_from(ob) {
|
if let Ok(l) = ob.downcast::<PyList>() {
|
||||||
match l.iter().map(SimpleJsonValue::extract).collect() {
|
match l.iter().map(SimpleJsonValue::extract).collect() {
|
||||||
Ok(a) => Ok(JsonValue::Array(a)),
|
Ok(a) => Ok(JsonValue::Array(a)),
|
||||||
Err(e) => Err(PyTypeError::new_err(format!(
|
Err(e) => Err(PyTypeError::new_err(format!(
|
||||||
|
@ -370,8 +370,8 @@ impl IntoPy<PyObject> for Condition {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'source> FromPyObject<'source> for Condition {
|
impl<'source> FromPyObject<'source> for Condition {
|
||||||
fn extract(ob: &'source PyAny) -> PyResult<Self> {
|
fn extract_bound(ob: &Bound<'source, PyAny>) -> PyResult<Self> {
|
||||||
Ok(depythonize(ob)?)
|
Ok(depythonize_bound(ob.clone())?)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
327
rust/src/rendezvous/mod.rs
Normal file
327
rust/src/rendezvous/mod.rs
Normal file
|
@ -0,0 +1,327 @@
|
||||||
|
/*
|
||||||
|
* This file is licensed under the Affero General Public License (AGPL) version 3.
|
||||||
|
*
|
||||||
|
* Copyright (C) 2024 New Vector, Ltd
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU Affero General Public License as
|
||||||
|
* published by the Free Software Foundation, either version 3 of the
|
||||||
|
* License, or (at your option) any later version.
|
||||||
|
*
|
||||||
|
* See the GNU Affero General Public License for more details:
|
||||||
|
* <https://www.gnu.org/licenses/agpl-3.0.html>.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
|
||||||
|
use std::{
|
||||||
|
collections::{BTreeMap, HashMap},
|
||||||
|
time::{Duration, SystemTime},
|
||||||
|
};
|
||||||
|
|
||||||
|
use bytes::Bytes;
|
||||||
|
use headers::{
|
||||||
|
AccessControlAllowOrigin, AccessControlExposeHeaders, CacheControl, ContentLength, ContentType,
|
||||||
|
HeaderMapExt, IfMatch, IfNoneMatch, Pragma,
|
||||||
|
};
|
||||||
|
use http::{header::ETAG, HeaderMap, Response, StatusCode, Uri};
|
||||||
|
use mime::Mime;
|
||||||
|
use pyo3::{
|
||||||
|
exceptions::PyValueError,
|
||||||
|
pyclass, pymethods,
|
||||||
|
types::{PyAnyMethods, PyModule, PyModuleMethods},
|
||||||
|
Bound, Py, PyAny, PyObject, PyResult, Python, ToPyObject,
|
||||||
|
};
|
||||||
|
use ulid::Ulid;
|
||||||
|
|
||||||
|
use self::session::Session;
|
||||||
|
use crate::{
|
||||||
|
errors::{NotFoundError, SynapseError},
|
||||||
|
http::{http_request_from_twisted, http_response_to_twisted, HeaderMapPyExt},
|
||||||
|
};
|
||||||
|
|
||||||
|
mod session;
|
||||||
|
|
||||||
|
// n.b. Because OPTIONS requests are handled by the Python code, we don't need to set Access-Control-Allow-Headers.
|
||||||
|
fn prepare_headers(headers: &mut HeaderMap, session: &Session) {
|
||||||
|
headers.typed_insert(AccessControlAllowOrigin::ANY);
|
||||||
|
headers.typed_insert(AccessControlExposeHeaders::from_iter([ETAG]));
|
||||||
|
headers.typed_insert(Pragma::no_cache());
|
||||||
|
headers.typed_insert(CacheControl::new().with_no_store());
|
||||||
|
headers.typed_insert(session.etag());
|
||||||
|
headers.typed_insert(session.expires());
|
||||||
|
headers.typed_insert(session.last_modified());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[pyclass]
|
||||||
|
struct RendezvousHandler {
|
||||||
|
base: Uri,
|
||||||
|
clock: PyObject,
|
||||||
|
sessions: BTreeMap<Ulid, Session>,
|
||||||
|
capacity: usize,
|
||||||
|
max_content_length: u64,
|
||||||
|
ttl: Duration,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl RendezvousHandler {
|
||||||
|
/// Check the input headers of a request which sets data for a session, and return the content type.
|
||||||
|
fn check_input_headers(&self, headers: &HeaderMap) -> PyResult<Mime> {
|
||||||
|
let ContentLength(content_length) = headers.typed_get_required()?;
|
||||||
|
|
||||||
|
if content_length > self.max_content_length {
|
||||||
|
return Err(SynapseError::new(
|
||||||
|
StatusCode::PAYLOAD_TOO_LARGE,
|
||||||
|
"Payload too large".to_owned(),
|
||||||
|
"M_TOO_LARGE",
|
||||||
|
None,
|
||||||
|
None,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
let content_type: ContentType = headers.typed_get_required()?;
|
||||||
|
|
||||||
|
// Content-Type must be text/plain
|
||||||
|
if content_type != ContentType::text() {
|
||||||
|
return Err(SynapseError::new(
|
||||||
|
StatusCode::BAD_REQUEST,
|
||||||
|
"Content-Type must be text/plain".to_owned(),
|
||||||
|
"M_INVALID_PARAM",
|
||||||
|
None,
|
||||||
|
None,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(content_type.into())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Evict expired sessions and remove the oldest sessions until we're under the capacity.
|
||||||
|
fn evict(&mut self, now: SystemTime) {
|
||||||
|
// First remove all the entries which expired
|
||||||
|
self.sessions.retain(|_, session| !session.expired(now));
|
||||||
|
|
||||||
|
// Then we remove the oldest entires until we're under the limit
|
||||||
|
while self.sessions.len() > self.capacity {
|
||||||
|
self.sessions.pop_first();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[pymethods]
|
||||||
|
impl RendezvousHandler {
|
||||||
|
#[new]
|
||||||
|
#[pyo3(signature = (homeserver, /, capacity=100, max_content_length=4*1024, eviction_interval=60*1000, ttl=60*1000))]
|
||||||
|
fn new(
|
||||||
|
py: Python<'_>,
|
||||||
|
homeserver: &Bound<'_, PyAny>,
|
||||||
|
capacity: usize,
|
||||||
|
max_content_length: u64,
|
||||||
|
eviction_interval: u64,
|
||||||
|
ttl: u64,
|
||||||
|
) -> PyResult<Py<Self>> {
|
||||||
|
let base: String = homeserver
|
||||||
|
.getattr("config")?
|
||||||
|
.getattr("server")?
|
||||||
|
.getattr("public_baseurl")?
|
||||||
|
.extract()?;
|
||||||
|
let base = Uri::try_from(format!("{base}_synapse/client/rendezvous"))
|
||||||
|
.map_err(|_| PyValueError::new_err("Invalid base URI"))?;
|
||||||
|
|
||||||
|
let clock = homeserver.call_method0("get_clock")?.to_object(py);
|
||||||
|
|
||||||
|
// Construct a Python object so that we can get a reference to the
|
||||||
|
// evict method and schedule it to run.
|
||||||
|
let self_ = Py::new(
|
||||||
|
py,
|
||||||
|
Self {
|
||||||
|
base,
|
||||||
|
clock,
|
||||||
|
sessions: BTreeMap::new(),
|
||||||
|
capacity,
|
||||||
|
max_content_length,
|
||||||
|
ttl: Duration::from_millis(ttl),
|
||||||
|
},
|
||||||
|
)?;
|
||||||
|
|
||||||
|
let evict = self_.getattr(py, "_evict")?;
|
||||||
|
homeserver.call_method0("get_clock")?.call_method(
|
||||||
|
"looping_call",
|
||||||
|
(evict, eviction_interval),
|
||||||
|
None,
|
||||||
|
)?;
|
||||||
|
|
||||||
|
Ok(self_)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn _evict(&mut self, py: Python<'_>) -> PyResult<()> {
|
||||||
|
let clock = self.clock.bind(py);
|
||||||
|
let now: u64 = clock.call_method0("time_msec")?.extract()?;
|
||||||
|
let now = SystemTime::UNIX_EPOCH + Duration::from_millis(now);
|
||||||
|
self.evict(now);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn handle_post(&mut self, py: Python<'_>, twisted_request: &Bound<'_, PyAny>) -> PyResult<()> {
|
||||||
|
let request = http_request_from_twisted(twisted_request)?;
|
||||||
|
|
||||||
|
let content_type = self.check_input_headers(request.headers())?;
|
||||||
|
|
||||||
|
let clock = self.clock.bind(py);
|
||||||
|
let now: u64 = clock.call_method0("time_msec")?.extract()?;
|
||||||
|
let now = SystemTime::UNIX_EPOCH + Duration::from_millis(now);
|
||||||
|
|
||||||
|
// We trigger an immediate eviction if we're at 2x the capacity
|
||||||
|
if self.sessions.len() >= self.capacity * 2 {
|
||||||
|
self.evict(now);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generate a new ULID for the session from the current time.
|
||||||
|
let id = Ulid::from_datetime(now);
|
||||||
|
|
||||||
|
let uri = format!("{base}/{id}", base = self.base);
|
||||||
|
|
||||||
|
let body = request.into_body();
|
||||||
|
|
||||||
|
let session = Session::new(body, content_type, now, self.ttl);
|
||||||
|
|
||||||
|
let response = serde_json::json!({
|
||||||
|
"url": uri,
|
||||||
|
})
|
||||||
|
.to_string();
|
||||||
|
|
||||||
|
let mut response = Response::new(response.as_bytes());
|
||||||
|
*response.status_mut() = StatusCode::CREATED;
|
||||||
|
response.headers_mut().typed_insert(ContentType::json());
|
||||||
|
prepare_headers(response.headers_mut(), &session);
|
||||||
|
http_response_to_twisted(twisted_request, response)?;
|
||||||
|
|
||||||
|
self.sessions.insert(id, session);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn handle_get(
|
||||||
|
&mut self,
|
||||||
|
py: Python<'_>,
|
||||||
|
twisted_request: &Bound<'_, PyAny>,
|
||||||
|
id: &str,
|
||||||
|
) -> PyResult<()> {
|
||||||
|
let request = http_request_from_twisted(twisted_request)?;
|
||||||
|
|
||||||
|
let if_none_match: Option<IfNoneMatch> = request.headers().typed_get_optional()?;
|
||||||
|
|
||||||
|
let now: u64 = self.clock.call_method0(py, "time_msec")?.extract(py)?;
|
||||||
|
let now = SystemTime::UNIX_EPOCH + Duration::from_millis(now);
|
||||||
|
|
||||||
|
let id: Ulid = id.parse().map_err(|_| NotFoundError::new())?;
|
||||||
|
let session = self
|
||||||
|
.sessions
|
||||||
|
.get(&id)
|
||||||
|
.filter(|s| !s.expired(now))
|
||||||
|
.ok_or_else(NotFoundError::new)?;
|
||||||
|
|
||||||
|
if let Some(if_none_match) = if_none_match {
|
||||||
|
if !if_none_match.precondition_passes(&session.etag()) {
|
||||||
|
let mut response = Response::new(Bytes::new());
|
||||||
|
*response.status_mut() = StatusCode::NOT_MODIFIED;
|
||||||
|
prepare_headers(response.headers_mut(), session);
|
||||||
|
http_response_to_twisted(twisted_request, response)?;
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut response = Response::new(session.data());
|
||||||
|
*response.status_mut() = StatusCode::OK;
|
||||||
|
let headers = response.headers_mut();
|
||||||
|
prepare_headers(headers, session);
|
||||||
|
headers.typed_insert(session.content_type());
|
||||||
|
headers.typed_insert(session.content_length());
|
||||||
|
http_response_to_twisted(twisted_request, response)?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn handle_put(
|
||||||
|
&mut self,
|
||||||
|
py: Python<'_>,
|
||||||
|
twisted_request: &Bound<'_, PyAny>,
|
||||||
|
id: &str,
|
||||||
|
) -> PyResult<()> {
|
||||||
|
let request = http_request_from_twisted(twisted_request)?;
|
||||||
|
|
||||||
|
let content_type = self.check_input_headers(request.headers())?;
|
||||||
|
|
||||||
|
let if_match: IfMatch = request.headers().typed_get_required()?;
|
||||||
|
|
||||||
|
let data = request.into_body();
|
||||||
|
|
||||||
|
let now: u64 = self.clock.call_method0(py, "time_msec")?.extract(py)?;
|
||||||
|
let now = SystemTime::UNIX_EPOCH + Duration::from_millis(now);
|
||||||
|
|
||||||
|
let id: Ulid = id.parse().map_err(|_| NotFoundError::new())?;
|
||||||
|
let session = self
|
||||||
|
.sessions
|
||||||
|
.get_mut(&id)
|
||||||
|
.filter(|s| !s.expired(now))
|
||||||
|
.ok_or_else(NotFoundError::new)?;
|
||||||
|
|
||||||
|
if !if_match.precondition_passes(&session.etag()) {
|
||||||
|
let mut headers = HeaderMap::new();
|
||||||
|
prepare_headers(&mut headers, session);
|
||||||
|
|
||||||
|
let mut additional_fields = HashMap::with_capacity(1);
|
||||||
|
additional_fields.insert(
|
||||||
|
String::from("org.matrix.msc4108.errcode"),
|
||||||
|
String::from("M_CONCURRENT_WRITE"),
|
||||||
|
);
|
||||||
|
|
||||||
|
return Err(SynapseError::new(
|
||||||
|
StatusCode::PRECONDITION_FAILED,
|
||||||
|
"ETag does not match".to_owned(),
|
||||||
|
"M_UNKNOWN", // Would be M_CONCURRENT_WRITE
|
||||||
|
Some(additional_fields),
|
||||||
|
Some(headers),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
session.update(data, content_type, now);
|
||||||
|
|
||||||
|
let mut response = Response::new(Bytes::new());
|
||||||
|
*response.status_mut() = StatusCode::ACCEPTED;
|
||||||
|
prepare_headers(response.headers_mut(), session);
|
||||||
|
http_response_to_twisted(twisted_request, response)?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn handle_delete(&mut self, twisted_request: &Bound<'_, PyAny>, id: &str) -> PyResult<()> {
|
||||||
|
let _request = http_request_from_twisted(twisted_request)?;
|
||||||
|
|
||||||
|
let id: Ulid = id.parse().map_err(|_| NotFoundError::new())?;
|
||||||
|
let _session = self.sessions.remove(&id).ok_or_else(NotFoundError::new)?;
|
||||||
|
|
||||||
|
let mut response = Response::new(Bytes::new());
|
||||||
|
*response.status_mut() = StatusCode::NO_CONTENT;
|
||||||
|
response
|
||||||
|
.headers_mut()
|
||||||
|
.typed_insert(AccessControlAllowOrigin::ANY);
|
||||||
|
http_response_to_twisted(twisted_request, response)?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn register_module(py: Python<'_>, m: &Bound<'_, PyModule>) -> PyResult<()> {
|
||||||
|
let child_module = PyModule::new_bound(py, "rendezvous")?;
|
||||||
|
|
||||||
|
child_module.add_class::<RendezvousHandler>()?;
|
||||||
|
|
||||||
|
m.add_submodule(&child_module)?;
|
||||||
|
|
||||||
|
// We need to manually add the module to sys.modules to make `from
|
||||||
|
// synapse.synapse_rust import rendezvous` work.
|
||||||
|
py.import_bound("sys")?
|
||||||
|
.getattr("modules")?
|
||||||
|
.set_item("synapse.synapse_rust.rendezvous", child_module)?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
91
rust/src/rendezvous/session.rs
Normal file
91
rust/src/rendezvous/session.rs
Normal file
|
@ -0,0 +1,91 @@
|
||||||
|
/*
|
||||||
|
* This file is licensed under the Affero General Public License (AGPL) version 3.
|
||||||
|
*
|
||||||
|
* Copyright (C) 2024 New Vector, Ltd
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU Affero General Public License as
|
||||||
|
* published by the Free Software Foundation, either version 3 of the
|
||||||
|
* License, or (at your option) any later version.
|
||||||
|
*
|
||||||
|
* See the GNU Affero General Public License for more details:
|
||||||
|
* <https://www.gnu.org/licenses/agpl-3.0.html>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
use std::time::{Duration, SystemTime};
|
||||||
|
|
||||||
|
use base64::{engine::general_purpose::URL_SAFE_NO_PAD, Engine as _};
|
||||||
|
use bytes::Bytes;
|
||||||
|
use headers::{ContentLength, ContentType, ETag, Expires, LastModified};
|
||||||
|
use mime::Mime;
|
||||||
|
use sha2::{Digest, Sha256};
|
||||||
|
|
||||||
|
/// A single session, containing data, metadata, and expiry information.
|
||||||
|
pub struct Session {
|
||||||
|
hash: [u8; 32],
|
||||||
|
data: Bytes,
|
||||||
|
content_type: Mime,
|
||||||
|
last_modified: SystemTime,
|
||||||
|
expires: SystemTime,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Session {
|
||||||
|
/// Create a new session with the given data, content type, and time-to-live.
|
||||||
|
pub fn new(data: Bytes, content_type: Mime, now: SystemTime, ttl: Duration) -> Self {
|
||||||
|
let hash = Sha256::digest(&data).into();
|
||||||
|
Self {
|
||||||
|
hash,
|
||||||
|
data,
|
||||||
|
content_type,
|
||||||
|
expires: now + ttl,
|
||||||
|
last_modified: now,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns true if the session has expired at the given time.
|
||||||
|
pub fn expired(&self, now: SystemTime) -> bool {
|
||||||
|
self.expires <= now
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Update the session with new data, content type, and last modified time.
|
||||||
|
pub fn update(&mut self, data: Bytes, content_type: Mime, now: SystemTime) {
|
||||||
|
self.hash = Sha256::digest(&data).into();
|
||||||
|
self.data = data;
|
||||||
|
self.content_type = content_type;
|
||||||
|
self.last_modified = now;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the Content-Type header of the session.
|
||||||
|
pub fn content_type(&self) -> ContentType {
|
||||||
|
self.content_type.clone().into()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the Content-Length header of the session.
|
||||||
|
pub fn content_length(&self) -> ContentLength {
|
||||||
|
ContentLength(self.data.len() as _)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the ETag header of the session.
|
||||||
|
pub fn etag(&self) -> ETag {
|
||||||
|
let encoded = URL_SAFE_NO_PAD.encode(self.hash);
|
||||||
|
// SAFETY: Base64 encoding is URL-safe, so ETag-safe
|
||||||
|
format!("\"{encoded}\"")
|
||||||
|
.parse()
|
||||||
|
.expect("base64-encoded hash should be URL-safe")
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the Last-Modified header of the session.
|
||||||
|
pub fn last_modified(&self) -> LastModified {
|
||||||
|
self.last_modified.into()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the Expires header of the session.
|
||||||
|
pub fn expires(&self) -> Expires {
|
||||||
|
self.expires.into()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the current data stored in the session.
|
||||||
|
pub fn data(&self) -> Bytes {
|
||||||
|
self.data.clone()
|
||||||
|
}
|
||||||
|
}
|
|
@ -214,7 +214,17 @@ fi
|
||||||
|
|
||||||
extra_test_args=()
|
extra_test_args=()
|
||||||
|
|
||||||
test_packages="./tests/csapi ./tests ./tests/msc3874 ./tests/msc3890 ./tests/msc3391 ./tests/msc3930 ./tests/msc3902"
|
test_packages=(
|
||||||
|
./tests/csapi
|
||||||
|
./tests
|
||||||
|
./tests/msc3874
|
||||||
|
./tests/msc3890
|
||||||
|
./tests/msc3391
|
||||||
|
./tests/msc3930
|
||||||
|
./tests/msc3902
|
||||||
|
./tests/msc3967
|
||||||
|
./tests/msc4115
|
||||||
|
)
|
||||||
|
|
||||||
# Enable dirty runs, so tests will reuse the same container where possible.
|
# Enable dirty runs, so tests will reuse the same container where possible.
|
||||||
# This significantly speeds up tests, but increases the possibility of test pollution.
|
# This significantly speeds up tests, but increases the possibility of test pollution.
|
||||||
|
@ -278,7 +288,7 @@ fi
|
||||||
export PASS_SYNAPSE_LOG_TESTING=1
|
export PASS_SYNAPSE_LOG_TESTING=1
|
||||||
|
|
||||||
# Run the tests!
|
# Run the tests!
|
||||||
echo "Images built; running complement with ${extra_test_args[@]} $@ $test_packages"
|
echo "Images built; running complement with ${extra_test_args[@]} $@ ${test_packages[@]}"
|
||||||
cd "$COMPLEMENT_DIR"
|
cd "$COMPLEMENT_DIR"
|
||||||
|
|
||||||
go test -v -tags "synapse_blacklist" -count=1 "${extra_test_args[@]}" "$@" $test_packages
|
go test -v -tags "synapse_blacklist" -count=1 "${extra_test_args[@]}" "$@" "${test_packages[@]}"
|
||||||
|
|
|
@ -91,7 +91,6 @@ else
|
||||||
"synapse" "docker" "tests"
|
"synapse" "docker" "tests"
|
||||||
"scripts-dev"
|
"scripts-dev"
|
||||||
"contrib" "synmark" "stubs" ".ci"
|
"contrib" "synmark" "stubs" ".ci"
|
||||||
"dev-docs"
|
|
||||||
)
|
)
|
||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
|
|
|
@ -127,7 +127,7 @@ BOOLEAN_COLUMNS = {
|
||||||
"redactions": ["have_censored"],
|
"redactions": ["have_censored"],
|
||||||
"room_stats_state": ["is_federatable"],
|
"room_stats_state": ["is_federatable"],
|
||||||
"rooms": ["is_public", "has_auth_chain_index"],
|
"rooms": ["is_public", "has_auth_chain_index"],
|
||||||
"users": ["shadow_banned", "approved", "locked"],
|
"users": ["shadow_banned", "approved", "locked", "suspended"],
|
||||||
"un_partial_stated_event_stream": ["rejection_status_changed"],
|
"un_partial_stated_event_stream": ["rejection_status_changed"],
|
||||||
"users_who_share_rooms": ["share_private"],
|
"users_who_share_rooms": ["share_private"],
|
||||||
"per_user_experimental_features": ["enabled"],
|
"per_user_experimental_features": ["enabled"],
|
||||||
|
@ -777,22 +777,74 @@ class Porter:
|
||||||
await self._setup_events_stream_seqs()
|
await self._setup_events_stream_seqs()
|
||||||
await self._setup_sequence(
|
await self._setup_sequence(
|
||||||
"un_partial_stated_event_stream_sequence",
|
"un_partial_stated_event_stream_sequence",
|
||||||
("un_partial_stated_event_stream",),
|
[("un_partial_stated_event_stream", "stream_id")],
|
||||||
)
|
)
|
||||||
await self._setup_sequence(
|
await self._setup_sequence(
|
||||||
"device_inbox_sequence", ("device_inbox", "device_federation_outbox")
|
"device_inbox_sequence",
|
||||||
|
[
|
||||||
|
("device_inbox", "stream_id"),
|
||||||
|
("device_federation_outbox", "stream_id"),
|
||||||
|
],
|
||||||
)
|
)
|
||||||
await self._setup_sequence(
|
await self._setup_sequence(
|
||||||
"account_data_sequence",
|
"account_data_sequence",
|
||||||
("room_account_data", "room_tags_revisions", "account_data"),
|
[
|
||||||
|
("room_account_data", "stream_id"),
|
||||||
|
("room_tags_revisions", "stream_id"),
|
||||||
|
("account_data", "stream_id"),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
await self._setup_sequence(
|
||||||
|
"receipts_sequence",
|
||||||
|
[
|
||||||
|
("receipts_linearized", "stream_id"),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
await self._setup_sequence(
|
||||||
|
"presence_stream_sequence",
|
||||||
|
[
|
||||||
|
("presence_stream", "stream_id"),
|
||||||
|
],
|
||||||
)
|
)
|
||||||
await self._setup_sequence("receipts_sequence", ("receipts_linearized",))
|
|
||||||
await self._setup_sequence("presence_stream_sequence", ("presence_stream",))
|
|
||||||
await self._setup_auth_chain_sequence()
|
await self._setup_auth_chain_sequence()
|
||||||
await self._setup_sequence(
|
await self._setup_sequence(
|
||||||
"application_services_txn_id_seq",
|
"application_services_txn_id_seq",
|
||||||
("application_services_txns",),
|
[
|
||||||
"txn_id",
|
(
|
||||||
|
"application_services_txns",
|
||||||
|
"txn_id",
|
||||||
|
)
|
||||||
|
],
|
||||||
|
)
|
||||||
|
await self._setup_sequence(
|
||||||
|
"device_lists_sequence",
|
||||||
|
[
|
||||||
|
("device_lists_stream", "stream_id"),
|
||||||
|
("user_signature_stream", "stream_id"),
|
||||||
|
("device_lists_outbound_pokes", "stream_id"),
|
||||||
|
("device_lists_changes_in_room", "stream_id"),
|
||||||
|
("device_lists_remote_pending", "stream_id"),
|
||||||
|
("device_lists_changes_converted_stream_position", "stream_id"),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
await self._setup_sequence(
|
||||||
|
"e2e_cross_signing_keys_sequence",
|
||||||
|
[
|
||||||
|
("e2e_cross_signing_keys", "stream_id"),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
await self._setup_sequence(
|
||||||
|
"push_rules_stream_sequence",
|
||||||
|
[
|
||||||
|
("push_rules_stream", "stream_id"),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
await self._setup_sequence(
|
||||||
|
"pushers_sequence",
|
||||||
|
[
|
||||||
|
("pushers", "id"),
|
||||||
|
("deleted_pushers", "stream_id"),
|
||||||
|
],
|
||||||
)
|
)
|
||||||
|
|
||||||
# Step 3. Get tables.
|
# Step 3. Get tables.
|
||||||
|
@ -1101,12 +1153,11 @@ class Porter:
|
||||||
async def _setup_sequence(
|
async def _setup_sequence(
|
||||||
self,
|
self,
|
||||||
sequence_name: str,
|
sequence_name: str,
|
||||||
stream_id_tables: Iterable[str],
|
stream_id_tables: Iterable[Tuple[str, str]],
|
||||||
column_name: str = "stream_id",
|
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Set a sequence to the correct value."""
|
"""Set a sequence to the correct value."""
|
||||||
current_stream_ids = []
|
current_stream_ids = []
|
||||||
for stream_id_table in stream_id_tables:
|
for stream_id_table, column_name in stream_id_tables:
|
||||||
max_stream_id = cast(
|
max_stream_id = cast(
|
||||||
int,
|
int,
|
||||||
await self.sqlite_store.db_pool.simple_select_one_onecol(
|
await self.sqlite_store.db_pool.simple_select_one_onecol(
|
||||||
|
|
|
@ -234,6 +234,13 @@ class EventContentFields:
|
||||||
TO_DEVICE_MSGID: Final = "org.matrix.msgid"
|
TO_DEVICE_MSGID: Final = "org.matrix.msgid"
|
||||||
|
|
||||||
|
|
||||||
|
class EventUnsignedContentFields:
|
||||||
|
"""Fields found inside the 'unsigned' data on events"""
|
||||||
|
|
||||||
|
# Requesting user's membership, per MSC4115
|
||||||
|
MSC4115_MEMBERSHIP: Final = "io.element.msc4115.membership"
|
||||||
|
|
||||||
|
|
||||||
class RoomTypes:
|
class RoomTypes:
|
||||||
"""Understood values of the room_type field of m.room.create events."""
|
"""Understood values of the room_type field of m.room.create events."""
|
||||||
|
|
||||||
|
|
|
@ -142,12 +142,12 @@ USER_FILTER_SCHEMA = {
|
||||||
|
|
||||||
@FormatChecker.cls_checks("matrix_room_id")
|
@FormatChecker.cls_checks("matrix_room_id")
|
||||||
def matrix_room_id_validator(room_id: object) -> bool:
|
def matrix_room_id_validator(room_id: object) -> bool:
|
||||||
return isinstance(room_id, str) and RoomID.is_valid(room_id)
|
return isinstance(room_id, str) and (RoomID.is_valid(room_id) or room_id == "*")
|
||||||
|
|
||||||
|
|
||||||
@FormatChecker.cls_checks("matrix_user_id")
|
@FormatChecker.cls_checks("matrix_user_id")
|
||||||
def matrix_user_id_validator(user_id: object) -> bool:
|
def matrix_user_id_validator(user_id: object) -> bool:
|
||||||
return isinstance(user_id, str) and UserID.is_valid(user_id)
|
return isinstance(user_id, str) and (UserID.is_valid(user_id) or user_id == "*")
|
||||||
|
|
||||||
|
|
||||||
class Filtering:
|
class Filtering:
|
||||||
|
|
|
@ -316,6 +316,10 @@ class Ratelimiter:
|
||||||
)
|
)
|
||||||
|
|
||||||
if not allowed:
|
if not allowed:
|
||||||
|
# We pause for a bit here to stop clients from "tight-looping" on
|
||||||
|
# retrying their request.
|
||||||
|
await self.clock.sleep(0.5)
|
||||||
|
|
||||||
raise LimitExceededError(
|
raise LimitExceededError(
|
||||||
limiter_name=self._limiter_name,
|
limiter_name=self._limiter_name,
|
||||||
retry_after_ms=int(1000 * (time_allowed - time_now_s)),
|
retry_after_ms=int(1000 * (time_allowed - time_now_s)),
|
||||||
|
|
|
@ -68,6 +68,7 @@ from synapse.config._base import format_config_error
|
||||||
from synapse.config.homeserver import HomeServerConfig
|
from synapse.config.homeserver import HomeServerConfig
|
||||||
from synapse.config.server import ListenerConfig, ManholeConfig, TCPListenerConfig
|
from synapse.config.server import ListenerConfig, ManholeConfig, TCPListenerConfig
|
||||||
from synapse.crypto import context_factory
|
from synapse.crypto import context_factory
|
||||||
|
from synapse.events.auto_accept_invites import InviteAutoAccepter
|
||||||
from synapse.events.presence_router import load_legacy_presence_router
|
from synapse.events.presence_router import load_legacy_presence_router
|
||||||
from synapse.handlers.auth import load_legacy_password_auth_providers
|
from synapse.handlers.auth import load_legacy_password_auth_providers
|
||||||
from synapse.http.site import SynapseSite
|
from synapse.http.site import SynapseSite
|
||||||
|
@ -582,6 +583,11 @@ async def start(hs: "HomeServer") -> None:
|
||||||
m = module(config, module_api)
|
m = module(config, module_api)
|
||||||
logger.info("Loaded module %s", m)
|
logger.info("Loaded module %s", m)
|
||||||
|
|
||||||
|
if hs.config.auto_accept_invites.enabled:
|
||||||
|
# Start the local auto_accept_invites module.
|
||||||
|
m = InviteAutoAccepter(hs.config.auto_accept_invites, module_api)
|
||||||
|
logger.info("Loaded local module %s", m)
|
||||||
|
|
||||||
load_legacy_spam_checkers(hs)
|
load_legacy_spam_checkers(hs)
|
||||||
load_legacy_third_party_event_rules(hs)
|
load_legacy_third_party_event_rules(hs)
|
||||||
load_legacy_presence_router(hs)
|
load_legacy_presence_router(hs)
|
||||||
|
@ -675,17 +681,17 @@ def setup_sentry(hs: "HomeServer") -> None:
|
||||||
)
|
)
|
||||||
|
|
||||||
# We set some default tags that give some context to this instance
|
# We set some default tags that give some context to this instance
|
||||||
with sentry_sdk.configure_scope() as scope:
|
global_scope = sentry_sdk.Scope.get_global_scope()
|
||||||
scope.set_tag("matrix_server_name", hs.config.server.server_name)
|
global_scope.set_tag("matrix_server_name", hs.config.server.server_name)
|
||||||
|
|
||||||
app = (
|
app = (
|
||||||
hs.config.worker.worker_app
|
hs.config.worker.worker_app
|
||||||
if hs.config.worker.worker_app
|
if hs.config.worker.worker_app
|
||||||
else "synapse.app.homeserver"
|
else "synapse.app.homeserver"
|
||||||
)
|
)
|
||||||
name = hs.get_instance_name()
|
name = hs.get_instance_name()
|
||||||
scope.set_tag("worker_app", app)
|
global_scope.set_tag("worker_app", app)
|
||||||
scope.set_tag("worker_name", name)
|
global_scope.set_tag("worker_name", name)
|
||||||
|
|
||||||
|
|
||||||
def setup_sdnotify(hs: "HomeServer") -> None:
|
def setup_sdnotify(hs: "HomeServer") -> None:
|
||||||
|
|
|
@ -23,6 +23,7 @@ from synapse.config import ( # noqa: F401
|
||||||
api,
|
api,
|
||||||
appservice,
|
appservice,
|
||||||
auth,
|
auth,
|
||||||
|
auto_accept_invites,
|
||||||
background_updates,
|
background_updates,
|
||||||
cache,
|
cache,
|
||||||
captcha,
|
captcha,
|
||||||
|
@ -35,6 +36,7 @@ from synapse.config import ( # noqa: F401
|
||||||
jwt,
|
jwt,
|
||||||
key,
|
key,
|
||||||
logger,
|
logger,
|
||||||
|
meow,
|
||||||
metrics,
|
metrics,
|
||||||
modules,
|
modules,
|
||||||
oembed,
|
oembed,
|
||||||
|
@ -91,6 +93,7 @@ class RootConfig:
|
||||||
voip: voip.VoipConfig
|
voip: voip.VoipConfig
|
||||||
registration: registration.RegistrationConfig
|
registration: registration.RegistrationConfig
|
||||||
account_validity: account_validity.AccountValidityConfig
|
account_validity: account_validity.AccountValidityConfig
|
||||||
|
meow: meow.MeowConfig
|
||||||
metrics: metrics.MetricsConfig
|
metrics: metrics.MetricsConfig
|
||||||
api: api.ApiConfig
|
api: api.ApiConfig
|
||||||
appservice: appservice.AppServiceConfig
|
appservice: appservice.AppServiceConfig
|
||||||
|
@ -120,6 +123,7 @@ class RootConfig:
|
||||||
federation: federation.FederationConfig
|
federation: federation.FederationConfig
|
||||||
retention: retention.RetentionConfig
|
retention: retention.RetentionConfig
|
||||||
background_updates: background_updates.BackgroundUpdateConfig
|
background_updates: background_updates.BackgroundUpdateConfig
|
||||||
|
auto_accept_invites: auto_accept_invites.AutoAcceptInvitesConfig
|
||||||
|
|
||||||
config_classes: List[Type["Config"]] = ...
|
config_classes: List[Type["Config"]] = ...
|
||||||
config_files: List[str]
|
config_files: List[str]
|
||||||
|
|
43
synapse/config/auto_accept_invites.py
Normal file
43
synapse/config/auto_accept_invites.py
Normal file
|
@ -0,0 +1,43 @@
|
||||||
|
#
|
||||||
|
# This file is licensed under the Affero General Public License (AGPL) version 3.
|
||||||
|
#
|
||||||
|
# Copyright (C) 2024 New Vector, Ltd
|
||||||
|
#
|
||||||
|
# This program is free software: you can redistribute it and/or modify
|
||||||
|
# it under the terms of the GNU Affero General Public License as
|
||||||
|
# published by the Free Software Foundation, either version 3 of the
|
||||||
|
# License, or (at your option) any later version.
|
||||||
|
#
|
||||||
|
# See the GNU Affero General Public License for more details:
|
||||||
|
# <https://www.gnu.org/licenses/agpl-3.0.html>.
|
||||||
|
#
|
||||||
|
# Originally licensed under the Apache License, Version 2.0:
|
||||||
|
# <http://www.apache.org/licenses/LICENSE-2.0>.
|
||||||
|
#
|
||||||
|
# [This file includes modifications made by New Vector Limited]
|
||||||
|
#
|
||||||
|
#
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
from synapse.types import JsonDict
|
||||||
|
|
||||||
|
from ._base import Config
|
||||||
|
|
||||||
|
|
||||||
|
class AutoAcceptInvitesConfig(Config):
|
||||||
|
section = "auto_accept_invites"
|
||||||
|
|
||||||
|
def read_config(self, config: JsonDict, **kwargs: Any) -> None:
|
||||||
|
auto_accept_invites_config = config.get("auto_accept_invites") or {}
|
||||||
|
|
||||||
|
self.enabled = auto_accept_invites_config.get("enabled", False)
|
||||||
|
|
||||||
|
self.accept_invites_only_for_direct_messages = auto_accept_invites_config.get(
|
||||||
|
"only_for_direct_messages", False
|
||||||
|
)
|
||||||
|
|
||||||
|
self.accept_invites_only_from_local_users = auto_accept_invites_config.get(
|
||||||
|
"only_from_local_users", False
|
||||||
|
)
|
||||||
|
|
||||||
|
self.worker_to_run_on = auto_accept_invites_config.get("worker_to_run_on")
|
|
@ -66,6 +66,17 @@ class CasConfig(Config):
|
||||||
|
|
||||||
self.cas_enable_registration = cas_config.get("enable_registration", True)
|
self.cas_enable_registration = cas_config.get("enable_registration", True)
|
||||||
|
|
||||||
|
self.cas_allow_numeric_ids = cas_config.get("allow_numeric_ids")
|
||||||
|
self.cas_numeric_ids_prefix = cas_config.get("numeric_ids_prefix")
|
||||||
|
if (
|
||||||
|
self.cas_numeric_ids_prefix is not None
|
||||||
|
and self.cas_numeric_ids_prefix.isalnum() is False
|
||||||
|
):
|
||||||
|
raise ConfigError(
|
||||||
|
"Only alphanumeric characters are allowed for numeric IDs prefix",
|
||||||
|
("cas_config", "numeric_ids_prefix"),
|
||||||
|
)
|
||||||
|
|
||||||
self.idp_name = cas_config.get("idp_name", "CAS")
|
self.idp_name = cas_config.get("idp_name", "CAS")
|
||||||
self.idp_icon = cas_config.get("idp_icon")
|
self.idp_icon = cas_config.get("idp_icon")
|
||||||
self.idp_brand = cas_config.get("idp_brand")
|
self.idp_brand = cas_config.get("idp_brand")
|
||||||
|
@ -77,6 +88,8 @@ class CasConfig(Config):
|
||||||
self.cas_displayname_attribute = None
|
self.cas_displayname_attribute = None
|
||||||
self.cas_required_attributes = []
|
self.cas_required_attributes = []
|
||||||
self.cas_enable_registration = False
|
self.cas_enable_registration = False
|
||||||
|
self.cas_allow_numeric_ids = False
|
||||||
|
self.cas_numeric_ids_prefix = "u"
|
||||||
|
|
||||||
|
|
||||||
# CAS uses a legacy required attributes mapping, not the one provided by
|
# CAS uses a legacy required attributes mapping, not the one provided by
|
||||||
|
|
|
@ -52,6 +52,7 @@ DEFAULT_SUBJECTS = {
|
||||||
"invite_from_person_to_space": "[%(app)s] %(person)s has invited you to join the %(space)s space on %(app)s...",
|
"invite_from_person_to_space": "[%(app)s] %(person)s has invited you to join the %(space)s space on %(app)s...",
|
||||||
"password_reset": "[%(server_name)s] Password reset",
|
"password_reset": "[%(server_name)s] Password reset",
|
||||||
"email_validation": "[%(server_name)s] Validate your email",
|
"email_validation": "[%(server_name)s] Validate your email",
|
||||||
|
"email_already_in_use": "[%(server_name)s] Email already in use",
|
||||||
}
|
}
|
||||||
|
|
||||||
LEGACY_TEMPLATE_DIR_WARNING = """
|
LEGACY_TEMPLATE_DIR_WARNING = """
|
||||||
|
@ -76,6 +77,7 @@ class EmailSubjectConfig:
|
||||||
invite_from_person_to_space: str
|
invite_from_person_to_space: str
|
||||||
password_reset: str
|
password_reset: str
|
||||||
email_validation: str
|
email_validation: str
|
||||||
|
email_already_in_use: str
|
||||||
|
|
||||||
|
|
||||||
class EmailConfig(Config):
|
class EmailConfig(Config):
|
||||||
|
@ -180,6 +182,12 @@ class EmailConfig(Config):
|
||||||
registration_template_text = email_config.get(
|
registration_template_text = email_config.get(
|
||||||
"registration_template_text", "registration.txt"
|
"registration_template_text", "registration.txt"
|
||||||
)
|
)
|
||||||
|
already_in_use_template_html = email_config.get(
|
||||||
|
"already_in_use_template_html", "already_in_use.html"
|
||||||
|
)
|
||||||
|
already_in_use_template_text = email_config.get(
|
||||||
|
"already_in_use_template_html", "already_in_use.txt"
|
||||||
|
)
|
||||||
add_threepid_template_html = email_config.get(
|
add_threepid_template_html = email_config.get(
|
||||||
"add_threepid_template_html", "add_threepid.html"
|
"add_threepid_template_html", "add_threepid.html"
|
||||||
)
|
)
|
||||||
|
@ -215,6 +223,8 @@ class EmailConfig(Config):
|
||||||
self.email_password_reset_template_text,
|
self.email_password_reset_template_text,
|
||||||
self.email_registration_template_html,
|
self.email_registration_template_html,
|
||||||
self.email_registration_template_text,
|
self.email_registration_template_text,
|
||||||
|
self.email_already_in_use_template_html,
|
||||||
|
self.email_already_in_use_template_text,
|
||||||
self.email_add_threepid_template_html,
|
self.email_add_threepid_template_html,
|
||||||
self.email_add_threepid_template_text,
|
self.email_add_threepid_template_text,
|
||||||
self.email_password_reset_template_confirmation_html,
|
self.email_password_reset_template_confirmation_html,
|
||||||
|
@ -230,6 +240,8 @@ class EmailConfig(Config):
|
||||||
password_reset_template_text,
|
password_reset_template_text,
|
||||||
registration_template_html,
|
registration_template_html,
|
||||||
registration_template_text,
|
registration_template_text,
|
||||||
|
already_in_use_template_html,
|
||||||
|
already_in_use_template_text,
|
||||||
add_threepid_template_html,
|
add_threepid_template_html,
|
||||||
add_threepid_template_text,
|
add_threepid_template_text,
|
||||||
"password_reset_confirmation.html",
|
"password_reset_confirmation.html",
|
||||||
|
|
|
@ -332,6 +332,9 @@ class ExperimentalConfig(Config):
|
||||||
# MSC3391: Removing account data.
|
# MSC3391: Removing account data.
|
||||||
self.msc3391_enabled = experimental.get("msc3391_enabled", False)
|
self.msc3391_enabled = experimental.get("msc3391_enabled", False)
|
||||||
|
|
||||||
|
# MSC3575 (Sliding Sync API endpoints)
|
||||||
|
self.msc3575_enabled: bool = experimental.get("msc3575_enabled", False)
|
||||||
|
|
||||||
# MSC3773: Thread notifications
|
# MSC3773: Thread notifications
|
||||||
self.msc3773_enabled: bool = experimental.get("msc3773_enabled", False)
|
self.msc3773_enabled: bool = experimental.get("msc3773_enabled", False)
|
||||||
|
|
||||||
|
@ -411,3 +414,32 @@ class ExperimentalConfig(Config):
|
||||||
self.msc4069_profile_inhibit_propagation = experimental.get(
|
self.msc4069_profile_inhibit_propagation = experimental.get(
|
||||||
"msc4069_profile_inhibit_propagation", False
|
"msc4069_profile_inhibit_propagation", False
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# MSC4108: Mechanism to allow OIDC sign in and E2EE set up via QR code
|
||||||
|
self.msc4108_enabled = experimental.get("msc4108_enabled", False)
|
||||||
|
|
||||||
|
self.msc4108_delegation_endpoint: Optional[str] = experimental.get(
|
||||||
|
"msc4108_delegation_endpoint", None
|
||||||
|
)
|
||||||
|
|
||||||
|
if (
|
||||||
|
self.msc4108_enabled or self.msc4108_delegation_endpoint is not None
|
||||||
|
) and not self.msc3861.enabled:
|
||||||
|
raise ConfigError(
|
||||||
|
"MSC4108 requires MSC3861 to be enabled",
|
||||||
|
("experimental", "msc4108_delegation_endpoint"),
|
||||||
|
)
|
||||||
|
|
||||||
|
if self.msc4108_delegation_endpoint is not None and self.msc4108_enabled:
|
||||||
|
raise ConfigError(
|
||||||
|
"You cannot have MSC4108 both enabled and delegated at the same time",
|
||||||
|
("experimental", "msc4108_delegation_endpoint"),
|
||||||
|
)
|
||||||
|
|
||||||
|
self.msc4115_membership_on_events = experimental.get(
|
||||||
|
"msc4115_membership_on_events", False
|
||||||
|
)
|
||||||
|
|
||||||
|
self.msc3916_authenticated_media_enabled = experimental.get(
|
||||||
|
"msc3916_authenticated_media_enabled", False
|
||||||
|
)
|
||||||
|
|
|
@ -42,6 +42,10 @@ class FederationConfig(Config):
|
||||||
for domain in federation_domain_whitelist:
|
for domain in federation_domain_whitelist:
|
||||||
self.federation_domain_whitelist[domain] = True
|
self.federation_domain_whitelist[domain] = True
|
||||||
|
|
||||||
|
self.federation_whitelist_endpoint_enabled = config.get(
|
||||||
|
"federation_whitelist_endpoint_enabled", False
|
||||||
|
)
|
||||||
|
|
||||||
federation_metrics_domains = config.get("federation_metrics_domains") or []
|
federation_metrics_domains = config.get("federation_metrics_domains") or []
|
||||||
validate_config(
|
validate_config(
|
||||||
_METRICS_FOR_DOMAINS_SCHEMA,
|
_METRICS_FOR_DOMAINS_SCHEMA,
|
||||||
|
|
|
@ -19,10 +19,12 @@
|
||||||
#
|
#
|
||||||
#
|
#
|
||||||
from ._base import RootConfig
|
from ._base import RootConfig
|
||||||
|
from .meow import MeowConfig
|
||||||
from .account_validity import AccountValidityConfig
|
from .account_validity import AccountValidityConfig
|
||||||
from .api import ApiConfig
|
from .api import ApiConfig
|
||||||
from .appservice import AppServiceConfig
|
from .appservice import AppServiceConfig
|
||||||
from .auth import AuthConfig
|
from .auth import AuthConfig
|
||||||
|
from .auto_accept_invites import AutoAcceptInvitesConfig
|
||||||
from .background_updates import BackgroundUpdateConfig
|
from .background_updates import BackgroundUpdateConfig
|
||||||
from .cache import CacheConfig
|
from .cache import CacheConfig
|
||||||
from .captcha import CaptchaConfig
|
from .captcha import CaptchaConfig
|
||||||
|
@ -64,6 +66,7 @@ from .workers import WorkerConfig
|
||||||
|
|
||||||
class HomeServerConfig(RootConfig):
|
class HomeServerConfig(RootConfig):
|
||||||
config_classes = [
|
config_classes = [
|
||||||
|
MeowConfig,
|
||||||
ModulesConfig,
|
ModulesConfig,
|
||||||
ServerConfig,
|
ServerConfig,
|
||||||
RetentionConfig,
|
RetentionConfig,
|
||||||
|
@ -105,4 +108,5 @@ class HomeServerConfig(RootConfig):
|
||||||
RedisConfig,
|
RedisConfig,
|
||||||
ExperimentalConfig,
|
ExperimentalConfig,
|
||||||
BackgroundUpdateConfig,
|
BackgroundUpdateConfig,
|
||||||
|
AutoAcceptInvitesConfig,
|
||||||
]
|
]
|
||||||
|
|
33
synapse/config/meow.py
Normal file
33
synapse/config/meow.py
Normal file
|
@ -0,0 +1,33 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# Copyright 2020 Maunium
|
||||||
|
#
|
||||||
|
# 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.
|
||||||
|
|
||||||
|
from ._base import Config
|
||||||
|
|
||||||
|
|
||||||
|
class MeowConfig(Config):
|
||||||
|
"""Meow Configuration
|
||||||
|
Configuration for disabling dumb limits in Synapse
|
||||||
|
"""
|
||||||
|
|
||||||
|
section = "meow"
|
||||||
|
|
||||||
|
def read_config(self, config, **kwargs):
|
||||||
|
meow_config = config.get("meow", {})
|
||||||
|
self.validation_override = set(meow_config.get("validation_override", []))
|
||||||
|
self.filter_override = set(meow_config.get("filter_override", []))
|
||||||
|
self.timestamp_override = set(meow_config.get("timestamp_override", []))
|
||||||
|
self.admin_api_register_invalid = meow_config.get(
|
||||||
|
"admin_api_register_invalid", True
|
||||||
|
)
|
|
@ -54,10 +54,8 @@ THUMBNAIL_SIZE_YAML = """\
|
||||||
THUMBNAIL_SUPPORTED_MEDIA_FORMAT_MAP = {
|
THUMBNAIL_SUPPORTED_MEDIA_FORMAT_MAP = {
|
||||||
"image/jpeg": "jpeg",
|
"image/jpeg": "jpeg",
|
||||||
"image/jpg": "jpeg",
|
"image/jpg": "jpeg",
|
||||||
"image/webp": "jpeg",
|
"image/webp": "webp",
|
||||||
# Thumbnails can only be jpeg or png. We choose png thumbnails for gif
|
"image/gif": "webp",
|
||||||
# because it can have transparency.
|
|
||||||
"image/gif": "png",
|
|
||||||
"image/png": "png",
|
"image/png": "png",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -109,6 +107,10 @@ def parse_thumbnail_requirements(
|
||||||
requirement.append(
|
requirement.append(
|
||||||
ThumbnailRequirement(width, height, method, "image/png")
|
ThumbnailRequirement(width, height, method, "image/png")
|
||||||
)
|
)
|
||||||
|
elif thumbnail_format == "webp":
|
||||||
|
requirement.append(
|
||||||
|
ThumbnailRequirement(width, height, method, "image/webp")
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
raise Exception(
|
raise Exception(
|
||||||
"Unknown thumbnail mapping from %s to %s. This is a Synapse problem, please report!"
|
"Unknown thumbnail mapping from %s to %s. This is a Synapse problem, please report!"
|
||||||
|
|
196
synapse/events/auto_accept_invites.py
Normal file
196
synapse/events/auto_accept_invites.py
Normal file
|
@ -0,0 +1,196 @@
|
||||||
|
#
|
||||||
|
# This file is licensed under the Affero General Public License (AGPL) version 3.
|
||||||
|
#
|
||||||
|
# Copyright 2021 The Matrix.org Foundation C.I.C
|
||||||
|
# Copyright (C) 2024 New Vector, Ltd
|
||||||
|
#
|
||||||
|
# This program is free software: you can redistribute it and/or modify
|
||||||
|
# it under the terms of the GNU Affero General Public License as
|
||||||
|
# published by the Free Software Foundation, either version 3 of the
|
||||||
|
# License, or (at your option) any later version.
|
||||||
|
#
|
||||||
|
# See the GNU Affero General Public License for more details:
|
||||||
|
# <https://www.gnu.org/licenses/agpl-3.0.html>.
|
||||||
|
#
|
||||||
|
# Originally licensed under the Apache License, Version 2.0:
|
||||||
|
# <http://www.apache.org/licenses/LICENSE-2.0>.
|
||||||
|
#
|
||||||
|
# [This file includes modifications made by New Vector Limited]
|
||||||
|
#
|
||||||
|
#
|
||||||
|
import logging
|
||||||
|
from http import HTTPStatus
|
||||||
|
from typing import Any, Dict, Tuple
|
||||||
|
|
||||||
|
from synapse.api.constants import AccountDataTypes, EventTypes, Membership
|
||||||
|
from synapse.api.errors import SynapseError
|
||||||
|
from synapse.config.auto_accept_invites import AutoAcceptInvitesConfig
|
||||||
|
from synapse.module_api import EventBase, ModuleApi, run_as_background_process
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class InviteAutoAccepter:
|
||||||
|
def __init__(self, config: AutoAcceptInvitesConfig, api: ModuleApi):
|
||||||
|
# Keep a reference to the Module API.
|
||||||
|
self._api = api
|
||||||
|
self._config = config
|
||||||
|
|
||||||
|
if not self._config.enabled:
|
||||||
|
return
|
||||||
|
|
||||||
|
should_run_on_this_worker = config.worker_to_run_on == self._api.worker_name
|
||||||
|
|
||||||
|
if not should_run_on_this_worker:
|
||||||
|
logger.info(
|
||||||
|
"Not accepting invites on this worker (configured: %r, here: %r)",
|
||||||
|
config.worker_to_run_on,
|
||||||
|
self._api.worker_name,
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
|
logger.info(
|
||||||
|
"Accepting invites on this worker (here: %r)", self._api.worker_name
|
||||||
|
)
|
||||||
|
|
||||||
|
# Register the callback.
|
||||||
|
self._api.register_third_party_rules_callbacks(
|
||||||
|
on_new_event=self.on_new_event,
|
||||||
|
)
|
||||||
|
|
||||||
|
async def on_new_event(self, event: EventBase, *args: Any) -> None:
|
||||||
|
"""Listens for new events, and if the event is an invite for a local user then
|
||||||
|
automatically accepts it.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
event: The incoming event.
|
||||||
|
"""
|
||||||
|
# Check if the event is an invite for a local user.
|
||||||
|
is_invite_for_local_user = (
|
||||||
|
event.type == EventTypes.Member
|
||||||
|
and event.is_state()
|
||||||
|
and event.membership == Membership.INVITE
|
||||||
|
and self._api.is_mine(event.state_key)
|
||||||
|
)
|
||||||
|
|
||||||
|
# Only accept invites for direct messages if the configuration mandates it.
|
||||||
|
is_direct_message = event.content.get("is_direct", False)
|
||||||
|
is_allowed_by_direct_message_rules = (
|
||||||
|
not self._config.accept_invites_only_for_direct_messages
|
||||||
|
or is_direct_message is True
|
||||||
|
)
|
||||||
|
|
||||||
|
# Only accept invites from remote users if the configuration mandates it.
|
||||||
|
is_from_local_user = self._api.is_mine(event.sender)
|
||||||
|
is_allowed_by_local_user_rules = (
|
||||||
|
not self._config.accept_invites_only_from_local_users
|
||||||
|
or is_from_local_user is True
|
||||||
|
)
|
||||||
|
|
||||||
|
if (
|
||||||
|
is_invite_for_local_user
|
||||||
|
and is_allowed_by_direct_message_rules
|
||||||
|
and is_allowed_by_local_user_rules
|
||||||
|
):
|
||||||
|
# Make the user join the room. We run this as a background process to circumvent a race condition
|
||||||
|
# that occurs when responding to invites over federation (see https://github.com/matrix-org/synapse-auto-accept-invite/issues/12)
|
||||||
|
run_as_background_process(
|
||||||
|
"retry_make_join",
|
||||||
|
self._retry_make_join,
|
||||||
|
event.state_key,
|
||||||
|
event.state_key,
|
||||||
|
event.room_id,
|
||||||
|
"join",
|
||||||
|
bg_start_span=False,
|
||||||
|
)
|
||||||
|
|
||||||
|
if is_direct_message:
|
||||||
|
# Mark this room as a direct message!
|
||||||
|
await self._mark_room_as_direct_message(
|
||||||
|
event.state_key, event.sender, event.room_id
|
||||||
|
)
|
||||||
|
|
||||||
|
async def _mark_room_as_direct_message(
|
||||||
|
self, user_id: str, dm_user_id: str, room_id: str
|
||||||
|
) -> None:
|
||||||
|
"""
|
||||||
|
Marks a room (`room_id`) as a direct message with the counterparty `dm_user_id`
|
||||||
|
from the perspective of the user `user_id`.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
user_id: the user for whom the membership is changing
|
||||||
|
dm_user_id: the user performing the membership change
|
||||||
|
room_id: room id of the room the user is invited to
|
||||||
|
"""
|
||||||
|
|
||||||
|
# This is a dict of User IDs to tuples of Room IDs
|
||||||
|
# (get_global will return a frozendict of tuples as it freezes the data,
|
||||||
|
# but we should accept either frozen or unfrozen variants.)
|
||||||
|
# Be careful: we convert the outer frozendict into a dict here,
|
||||||
|
# but the contents of the dict are still frozen (tuples in lieu of lists,
|
||||||
|
# etc.)
|
||||||
|
dm_map: Dict[str, Tuple[str, ...]] = dict(
|
||||||
|
await self._api.account_data_manager.get_global(
|
||||||
|
user_id, AccountDataTypes.DIRECT
|
||||||
|
)
|
||||||
|
or {}
|
||||||
|
)
|
||||||
|
|
||||||
|
if dm_user_id not in dm_map:
|
||||||
|
dm_map[dm_user_id] = (room_id,)
|
||||||
|
else:
|
||||||
|
dm_rooms_for_user = dm_map[dm_user_id]
|
||||||
|
assert isinstance(dm_rooms_for_user, (tuple, list))
|
||||||
|
|
||||||
|
dm_map[dm_user_id] = tuple(dm_rooms_for_user) + (room_id,)
|
||||||
|
|
||||||
|
await self._api.account_data_manager.put_global(
|
||||||
|
user_id, AccountDataTypes.DIRECT, dm_map
|
||||||
|
)
|
||||||
|
|
||||||
|
async def _retry_make_join(
|
||||||
|
self, sender: str, target: str, room_id: str, new_membership: str
|
||||||
|
) -> None:
|
||||||
|
"""
|
||||||
|
A function to retry sending the `make_join` request with an increasing backoff. This is
|
||||||
|
implemented to work around a race condition when receiving invites over federation.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
sender: the user performing the membership change
|
||||||
|
target: the user for whom the membership is changing
|
||||||
|
room_id: room id of the room to join to
|
||||||
|
new_membership: the type of membership event (in this case will be "join")
|
||||||
|
"""
|
||||||
|
|
||||||
|
sleep = 0
|
||||||
|
retries = 0
|
||||||
|
join_event = None
|
||||||
|
|
||||||
|
while retries < 5:
|
||||||
|
try:
|
||||||
|
await self._api.sleep(sleep)
|
||||||
|
join_event = await self._api.update_room_membership(
|
||||||
|
sender=sender,
|
||||||
|
target=target,
|
||||||
|
room_id=room_id,
|
||||||
|
new_membership=new_membership,
|
||||||
|
)
|
||||||
|
except SynapseError as e:
|
||||||
|
if e.code == HTTPStatus.FORBIDDEN:
|
||||||
|
logger.debug(
|
||||||
|
f"Update_room_membership was forbidden. This can sometimes be expected for remote invites. Exception: {e}"
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
logger.warn(
|
||||||
|
f"Update_room_membership raised the following unexpected (SynapseError) exception: {e}"
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
logger.warn(
|
||||||
|
f"Update_room_membership raised the following unexpected exception: {e}"
|
||||||
|
)
|
||||||
|
|
||||||
|
sleep = 2**retries
|
||||||
|
retries += 1
|
||||||
|
|
||||||
|
if join_event is not None:
|
||||||
|
break
|
|
@ -49,7 +49,7 @@ from synapse.api.errors import Codes, SynapseError
|
||||||
from synapse.api.room_versions import RoomVersion
|
from synapse.api.room_versions import RoomVersion
|
||||||
from synapse.types import JsonDict, Requester
|
from synapse.types import JsonDict, Requester
|
||||||
|
|
||||||
from . import EventBase
|
from . import EventBase, make_event_from_dict
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from synapse.handlers.relations import BundledAggregations
|
from synapse.handlers.relations import BundledAggregations
|
||||||
|
@ -82,17 +82,14 @@ def prune_event(event: EventBase) -> EventBase:
|
||||||
"""
|
"""
|
||||||
pruned_event_dict = prune_event_dict(event.room_version, event.get_dict())
|
pruned_event_dict = prune_event_dict(event.room_version, event.get_dict())
|
||||||
|
|
||||||
from . import make_event_from_dict
|
|
||||||
|
|
||||||
pruned_event = make_event_from_dict(
|
pruned_event = make_event_from_dict(
|
||||||
pruned_event_dict, event.room_version, event.internal_metadata.get_dict()
|
pruned_event_dict, event.room_version, event.internal_metadata.get_dict()
|
||||||
)
|
)
|
||||||
|
|
||||||
# copy the internal fields
|
# Copy the bits of `internal_metadata` that aren't returned by `get_dict`
|
||||||
pruned_event.internal_metadata.stream_ordering = (
|
pruned_event.internal_metadata.stream_ordering = (
|
||||||
event.internal_metadata.stream_ordering
|
event.internal_metadata.stream_ordering
|
||||||
)
|
)
|
||||||
|
|
||||||
pruned_event.internal_metadata.outlier = event.internal_metadata.outlier
|
pruned_event.internal_metadata.outlier = event.internal_metadata.outlier
|
||||||
|
|
||||||
# Mark the event as redacted
|
# Mark the event as redacted
|
||||||
|
@ -101,6 +98,29 @@ def prune_event(event: EventBase) -> EventBase:
|
||||||
return pruned_event
|
return pruned_event
|
||||||
|
|
||||||
|
|
||||||
|
def clone_event(event: EventBase) -> EventBase:
|
||||||
|
"""Take a copy of the event.
|
||||||
|
|
||||||
|
This is mostly useful because it does a *shallow* copy of the `unsigned` data,
|
||||||
|
which means it can then be updated without corrupting the in-memory cache. Note that
|
||||||
|
other properties of the event, such as `content`, are *not* (currently) copied here.
|
||||||
|
"""
|
||||||
|
# XXX: We rely on at least one of `event.get_dict()` and `make_event_from_dict()`
|
||||||
|
# making a copy of `unsigned`. Currently, both do, though I don't really know why.
|
||||||
|
# Still, as long as they do, there's not much point doing yet another copy here.
|
||||||
|
new_event = make_event_from_dict(
|
||||||
|
event.get_dict(), event.room_version, event.internal_metadata.get_dict()
|
||||||
|
)
|
||||||
|
|
||||||
|
# Copy the bits of `internal_metadata` that aren't returned by `get_dict`.
|
||||||
|
new_event.internal_metadata.stream_ordering = (
|
||||||
|
event.internal_metadata.stream_ordering
|
||||||
|
)
|
||||||
|
new_event.internal_metadata.outlier = event.internal_metadata.outlier
|
||||||
|
|
||||||
|
return new_event
|
||||||
|
|
||||||
|
|
||||||
def prune_event_dict(room_version: RoomVersion, event_dict: JsonDict) -> JsonDict:
|
def prune_event_dict(room_version: RoomVersion, event_dict: JsonDict) -> JsonDict:
|
||||||
"""Redacts the event_dict in the same way as `prune_event`, except it
|
"""Redacts the event_dict in the same way as `prune_event`, except it
|
||||||
operates on dicts rather than event objects
|
operates on dicts rather than event objects
|
||||||
|
@ -485,6 +505,11 @@ def serialize_event(
|
||||||
):
|
):
|
||||||
d["unsigned"]["transaction_id"] = txn_id
|
d["unsigned"]["transaction_id"] = txn_id
|
||||||
|
|
||||||
|
# Beeper: include internal stream ordering as HS order unsigned hint
|
||||||
|
stream_ordering = getattr(e.internal_metadata, "stream_ordering", None)
|
||||||
|
if stream_ordering:
|
||||||
|
d["unsigned"]["com.beeper.hs.order"] = stream_ordering
|
||||||
|
|
||||||
# invite_room_state and knock_room_state are a list of stripped room state events
|
# invite_room_state and knock_room_state are a list of stripped room state events
|
||||||
# that are meant to provide metadata about a room to an invitee/knocker. They are
|
# that are meant to provide metadata about a room to an invitee/knocker. They are
|
||||||
# intended to only be included in specific circumstances, such as down sync, and
|
# intended to only be included in specific circumstances, such as down sync, and
|
||||||
|
|
|
@ -64,7 +64,7 @@ class EventValidator:
|
||||||
event: The event to validate.
|
event: The event to validate.
|
||||||
config: The homeserver's configuration.
|
config: The homeserver's configuration.
|
||||||
"""
|
"""
|
||||||
self.validate_builder(event)
|
self.validate_builder(event, config)
|
||||||
|
|
||||||
if event.format_version == EventFormatVersions.ROOM_V1_V2:
|
if event.format_version == EventFormatVersions.ROOM_V1_V2:
|
||||||
EventID.from_string(event.event_id)
|
EventID.from_string(event.event_id)
|
||||||
|
@ -95,6 +95,12 @@ class EventValidator:
|
||||||
# Note that only the client controlled portion of the event is
|
# Note that only the client controlled portion of the event is
|
||||||
# checked, since we trust the portions of the event we created.
|
# checked, since we trust the portions of the event we created.
|
||||||
validate_canonicaljson(event.content)
|
validate_canonicaljson(event.content)
|
||||||
|
if not 0 < event.origin_server_ts < 2**53:
|
||||||
|
raise SynapseError(400, "Event timestamp is out of range")
|
||||||
|
|
||||||
|
# meow: allow specific users to send potentially dangerous events.
|
||||||
|
if event.sender in config.meow.validation_override:
|
||||||
|
return
|
||||||
|
|
||||||
if event.type == EventTypes.Aliases:
|
if event.type == EventTypes.Aliases:
|
||||||
if "aliases" in event.content:
|
if "aliases" in event.content:
|
||||||
|
@ -193,7 +199,9 @@ class EventValidator:
|
||||||
errcode=Codes.BAD_JSON,
|
errcode=Codes.BAD_JSON,
|
||||||
)
|
)
|
||||||
|
|
||||||
def validate_builder(self, event: Union[EventBase, EventBuilder]) -> None:
|
def validate_builder(
|
||||||
|
self, event: Union[EventBase, EventBuilder], config: HomeServerConfig
|
||||||
|
) -> None:
|
||||||
"""Validates that the builder/event has roughly the right format. Only
|
"""Validates that the builder/event has roughly the right format. Only
|
||||||
checks values that we expect a proto event to have, rather than all the
|
checks values that we expect a proto event to have, rather than all the
|
||||||
fields an event would have
|
fields an event would have
|
||||||
|
@ -211,6 +219,10 @@ class EventValidator:
|
||||||
RoomID.from_string(event.room_id)
|
RoomID.from_string(event.room_id)
|
||||||
UserID.from_string(event.sender)
|
UserID.from_string(event.sender)
|
||||||
|
|
||||||
|
# meow: allow specific users to send so-called invalid events
|
||||||
|
if event.sender in config.meow.validation_override:
|
||||||
|
return
|
||||||
|
|
||||||
if event.type == EventTypes.Message:
|
if event.type == EventTypes.Message:
|
||||||
strings = ["body", "msgtype"]
|
strings = ["body", "msgtype"]
|
||||||
|
|
||||||
|
|
|
@ -546,7 +546,25 @@ class FederationServer(FederationBase):
|
||||||
edu_type=edu_dict["edu_type"],
|
edu_type=edu_dict["edu_type"],
|
||||||
content=edu_dict["content"],
|
content=edu_dict["content"],
|
||||||
)
|
)
|
||||||
await self.registry.on_edu(edu.edu_type, origin, edu.content)
|
try:
|
||||||
|
await self.registry.on_edu(edu.edu_type, origin, edu.content)
|
||||||
|
except Exception:
|
||||||
|
# If there was an error handling the EDU, we must reject the
|
||||||
|
# transaction.
|
||||||
|
#
|
||||||
|
# Some EDU types (notably, to-device messages) are, despite their name,
|
||||||
|
# expected to be reliable; if we weren't able to do something with it,
|
||||||
|
# we have to tell the sender that, and the only way the protocol gives
|
||||||
|
# us to do so is by sending an HTTP error back on the transaction.
|
||||||
|
#
|
||||||
|
# We log the exception now, and then raise a new SynapseError to cause
|
||||||
|
# the transaction to be failed.
|
||||||
|
logger.exception("Error handling EDU of type %s", edu.edu_type)
|
||||||
|
raise SynapseError(500, f"Error handing EDU of type {edu.edu_type}")
|
||||||
|
|
||||||
|
# TODO: if the first EDU fails, we should probably abort the whole
|
||||||
|
# thing rather than carrying on with the rest of them. That would
|
||||||
|
# probably be best done inside `concurrently_execute`.
|
||||||
|
|
||||||
await concurrently_execute(
|
await concurrently_execute(
|
||||||
_process_edu,
|
_process_edu,
|
||||||
|
@ -1414,12 +1432,7 @@ class FederationHandlerRegistry:
|
||||||
handler = self.edu_handlers.get(edu_type)
|
handler = self.edu_handlers.get(edu_type)
|
||||||
if handler:
|
if handler:
|
||||||
with start_active_span_from_edu(content, "handle_edu"):
|
with start_active_span_from_edu(content, "handle_edu"):
|
||||||
try:
|
await handler(origin, content)
|
||||||
await handler(origin, content)
|
|
||||||
except SynapseError as e:
|
|
||||||
logger.info("Failed to handle edu %r: %r", edu_type, e)
|
|
||||||
except Exception:
|
|
||||||
logger.exception("Failed to handle edu %r", edu_type)
|
|
||||||
return
|
return
|
||||||
|
|
||||||
# Check if we can route it somewhere else that isn't us
|
# Check if we can route it somewhere else that isn't us
|
||||||
|
@ -1428,17 +1441,12 @@ class FederationHandlerRegistry:
|
||||||
# Pick an instance randomly so that we don't overload one.
|
# Pick an instance randomly so that we don't overload one.
|
||||||
route_to = random.choice(instances)
|
route_to = random.choice(instances)
|
||||||
|
|
||||||
try:
|
await self._send_edu(
|
||||||
await self._send_edu(
|
instance_name=route_to,
|
||||||
instance_name=route_to,
|
edu_type=edu_type,
|
||||||
edu_type=edu_type,
|
origin=origin,
|
||||||
origin=origin,
|
content=content,
|
||||||
content=content,
|
)
|
||||||
)
|
|
||||||
except SynapseError as e:
|
|
||||||
logger.info("Failed to handle edu %r: %r", edu_type, e)
|
|
||||||
except Exception:
|
|
||||||
logger.exception("Failed to handle edu %r", edu_type)
|
|
||||||
return
|
return
|
||||||
|
|
||||||
# Oh well, let's just log and move on.
|
# Oh well, let's just log and move on.
|
||||||
|
|
|
@ -180,7 +180,11 @@ def _parse_auth_header(header_bytes: bytes) -> Tuple[str, str, str, Optional[str
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
header_str = header_bytes.decode("utf-8")
|
header_str = header_bytes.decode("utf-8")
|
||||||
params = re.split(" +", header_str)[1].split(",")
|
space_or_tab = "[ \t]"
|
||||||
|
params = re.split(
|
||||||
|
rf"{space_or_tab}*,{space_or_tab}*",
|
||||||
|
re.split(r"^X-Matrix +", header_str, maxsplit=1)[1],
|
||||||
|
)
|
||||||
param_dict: Dict[str, str] = {
|
param_dict: Dict[str, str] = {
|
||||||
k.lower(): v for k, v in [param.split("=", maxsplit=1) for param in params]
|
k.lower(): v for k, v in [param.split("=", maxsplit=1) for param in params]
|
||||||
}
|
}
|
||||||
|
|
|
@ -42,6 +42,7 @@ class AdminHandler:
|
||||||
self._device_handler = hs.get_device_handler()
|
self._device_handler = hs.get_device_handler()
|
||||||
self._storage_controllers = hs.get_storage_controllers()
|
self._storage_controllers = hs.get_storage_controllers()
|
||||||
self._state_storage_controller = self._storage_controllers.state
|
self._state_storage_controller = self._storage_controllers.state
|
||||||
|
self._hs_config = hs.config
|
||||||
self._msc3866_enabled = hs.config.experimental.msc3866.enabled
|
self._msc3866_enabled = hs.config.experimental.msc3866.enabled
|
||||||
|
|
||||||
async def get_whois(self, user: UserID) -> JsonMapping:
|
async def get_whois(self, user: UserID) -> JsonMapping:
|
||||||
|
@ -217,7 +218,10 @@ class AdminHandler:
|
||||||
)
|
)
|
||||||
|
|
||||||
events = await filter_events_for_client(
|
events = await filter_events_for_client(
|
||||||
self._storage_controllers, user_id, events
|
self._storage_controllers,
|
||||||
|
user_id,
|
||||||
|
events,
|
||||||
|
msc4115_membership_on_events=self._hs_config.experimental.msc4115_membership_on_events,
|
||||||
)
|
)
|
||||||
|
|
||||||
writer.write_events(room_id, events)
|
writer.write_events(room_id, events)
|
||||||
|
|
|
@ -78,6 +78,8 @@ class CasHandler:
|
||||||
self._cas_displayname_attribute = hs.config.cas.cas_displayname_attribute
|
self._cas_displayname_attribute = hs.config.cas.cas_displayname_attribute
|
||||||
self._cas_required_attributes = hs.config.cas.cas_required_attributes
|
self._cas_required_attributes = hs.config.cas.cas_required_attributes
|
||||||
self._cas_enable_registration = hs.config.cas.cas_enable_registration
|
self._cas_enable_registration = hs.config.cas.cas_enable_registration
|
||||||
|
self._cas_allow_numeric_ids = hs.config.cas.cas_allow_numeric_ids
|
||||||
|
self._cas_numeric_ids_prefix = hs.config.cas.cas_numeric_ids_prefix
|
||||||
|
|
||||||
self._http_client = hs.get_proxied_http_client()
|
self._http_client = hs.get_proxied_http_client()
|
||||||
|
|
||||||
|
@ -188,6 +190,9 @@ class CasHandler:
|
||||||
for child in root[0]:
|
for child in root[0]:
|
||||||
if child.tag.endswith("user"):
|
if child.tag.endswith("user"):
|
||||||
user = child.text
|
user = child.text
|
||||||
|
# if numeric user IDs are allowed and username is numeric then we add the prefix so Synapse can handle it
|
||||||
|
if self._cas_allow_numeric_ids and user is not None and user.isdigit():
|
||||||
|
user = f"{self._cas_numeric_ids_prefix}{user}"
|
||||||
if child.tag.endswith("attributes"):
|
if child.tag.endswith("attributes"):
|
||||||
for attribute in child:
|
for attribute in child:
|
||||||
# ElementTree library expands the namespace in
|
# ElementTree library expands the namespace in
|
||||||
|
|
|
@ -261,11 +261,22 @@ class DeactivateAccountHandler:
|
||||||
user = UserID.from_string(user_id)
|
user = UserID.from_string(user_id)
|
||||||
|
|
||||||
rooms_for_user = await self.store.get_rooms_for_user(user_id)
|
rooms_for_user = await self.store.get_rooms_for_user(user_id)
|
||||||
|
requester = create_requester(user, authenticated_entity=self._server_name)
|
||||||
|
should_erase = await self.store.is_user_erased(user_id)
|
||||||
|
|
||||||
for room_id in rooms_for_user:
|
for room_id in rooms_for_user:
|
||||||
logger.info("User parter parting %r from %r", user_id, room_id)
|
logger.info("User parter parting %r from %r", user_id, room_id)
|
||||||
try:
|
try:
|
||||||
|
# Before parting the user, redact all membership events if requested
|
||||||
|
if should_erase:
|
||||||
|
event_ids = await self.store.get_membership_event_ids_for_user(
|
||||||
|
user_id, room_id
|
||||||
|
)
|
||||||
|
for event_id in event_ids:
|
||||||
|
await self.store.expire_event(event_id)
|
||||||
|
|
||||||
await self._room_member_handler.update_membership(
|
await self._room_member_handler.update_membership(
|
||||||
create_requester(user, authenticated_entity=self._server_name),
|
requester,
|
||||||
user,
|
user,
|
||||||
room_id,
|
room_id,
|
||||||
"leave",
|
"leave",
|
||||||
|
|
|
@ -159,20 +159,32 @@ class DeviceWorkerHandler:
|
||||||
|
|
||||||
@cancellable
|
@cancellable
|
||||||
async def get_device_changes_in_shared_rooms(
|
async def get_device_changes_in_shared_rooms(
|
||||||
self, user_id: str, room_ids: StrCollection, from_token: StreamToken
|
self,
|
||||||
|
user_id: str,
|
||||||
|
room_ids: StrCollection,
|
||||||
|
from_token: StreamToken,
|
||||||
|
now_token: Optional[StreamToken] = None,
|
||||||
) -> Set[str]:
|
) -> Set[str]:
|
||||||
"""Get the set of users whose devices have changed who share a room with
|
"""Get the set of users whose devices have changed who share a room with
|
||||||
the given user.
|
the given user.
|
||||||
"""
|
"""
|
||||||
|
now_device_lists_key = self.store.get_device_stream_token()
|
||||||
|
if now_token:
|
||||||
|
now_device_lists_key = now_token.device_list_key
|
||||||
|
|
||||||
changed_users = await self.store.get_device_list_changes_in_rooms(
|
changed_users = await self.store.get_device_list_changes_in_rooms(
|
||||||
room_ids, from_token.device_list_key
|
room_ids,
|
||||||
|
from_token.device_list_key,
|
||||||
|
now_device_lists_key,
|
||||||
)
|
)
|
||||||
|
|
||||||
if changed_users is not None:
|
if changed_users is not None:
|
||||||
# We also check if the given user has changed their device. If
|
# We also check if the given user has changed their device. If
|
||||||
# they're in no rooms then the above query won't include them.
|
# they're in no rooms then the above query won't include them.
|
||||||
changed = await self.store.get_users_whose_devices_changed(
|
changed = await self.store.get_users_whose_devices_changed(
|
||||||
from_token.device_list_key, [user_id]
|
from_token.device_list_key,
|
||||||
|
[user_id],
|
||||||
|
to_key=now_device_lists_key,
|
||||||
)
|
)
|
||||||
changed_users.update(changed)
|
changed_users.update(changed)
|
||||||
return changed_users
|
return changed_users
|
||||||
|
@ -190,7 +202,9 @@ class DeviceWorkerHandler:
|
||||||
tracked_users.add(user_id)
|
tracked_users.add(user_id)
|
||||||
|
|
||||||
changed = await self.store.get_users_whose_devices_changed(
|
changed = await self.store.get_users_whose_devices_changed(
|
||||||
from_token.device_list_key, tracked_users
|
from_token.device_list_key,
|
||||||
|
tracked_users,
|
||||||
|
to_key=now_device_lists_key,
|
||||||
)
|
)
|
||||||
|
|
||||||
return changed
|
return changed
|
||||||
|
@ -892,6 +906,13 @@ class DeviceHandler(DeviceWorkerHandler):
|
||||||
context=opentracing_context,
|
context=opentracing_context,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
await self.store.mark_redundant_device_lists_pokes(
|
||||||
|
user_id=user_id,
|
||||||
|
device_id=device_id,
|
||||||
|
room_id=room_id,
|
||||||
|
converted_upto_stream_id=stream_id,
|
||||||
|
)
|
||||||
|
|
||||||
# Notify replication that we've updated the device list stream.
|
# Notify replication that we've updated the device list stream.
|
||||||
self.notifier.notify_replication()
|
self.notifier.notify_replication()
|
||||||
|
|
||||||
|
|
|
@ -104,6 +104,9 @@ class DeviceMessageHandler:
|
||||||
"""
|
"""
|
||||||
Handle receiving to-device messages from remote homeservers.
|
Handle receiving to-device messages from remote homeservers.
|
||||||
|
|
||||||
|
Note that any errors thrown from this method will cause the federation /send
|
||||||
|
request to receive an error response.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
origin: The remote homeserver.
|
origin: The remote homeserver.
|
||||||
content: The JSON dictionary containing the to-device messages.
|
content: The JSON dictionary containing the to-device messages.
|
||||||
|
@ -233,6 +236,13 @@ class DeviceMessageHandler:
|
||||||
local_messages = {}
|
local_messages = {}
|
||||||
remote_messages: Dict[str, Dict[str, Dict[str, JsonDict]]] = {}
|
remote_messages: Dict[str, Dict[str, Dict[str, JsonDict]]] = {}
|
||||||
for user_id, by_device in messages.items():
|
for user_id, by_device in messages.items():
|
||||||
|
if not UserID.is_valid(user_id):
|
||||||
|
logger.warning(
|
||||||
|
"Ignoring attempt to send device message to invalid user: %r",
|
||||||
|
user_id,
|
||||||
|
)
|
||||||
|
continue
|
||||||
|
|
||||||
# add an opentracing log entry for each message
|
# add an opentracing log entry for each message
|
||||||
for device_id, message_content in by_device.items():
|
for device_id, message_content in by_device.items():
|
||||||
log_kv(
|
log_kv(
|
||||||
|
|
|
@ -80,9 +80,11 @@ class DirectoryHandler:
|
||||||
) -> None:
|
) -> None:
|
||||||
# general association creation for both human users and app services
|
# general association creation for both human users and app services
|
||||||
|
|
||||||
for wchar in string.whitespace:
|
# meow: allow specific users to include anything in room aliases
|
||||||
if wchar in room_alias.localpart:
|
if creator not in self.config.meow.validation_override:
|
||||||
raise SynapseError(400, "Invalid characters in room alias")
|
for wchar in string.whitespace:
|
||||||
|
if wchar in room_alias.localpart:
|
||||||
|
raise SynapseError(400, "Invalid characters in room alias")
|
||||||
|
|
||||||
if ":" in room_alias.localpart:
|
if ":" in room_alias.localpart:
|
||||||
raise SynapseError(400, "Invalid character in room alias localpart: ':'.")
|
raise SynapseError(400, "Invalid character in room alias localpart: ':'.")
|
||||||
|
@ -127,7 +129,10 @@ class DirectoryHandler:
|
||||||
user_id = requester.user.to_string()
|
user_id = requester.user.to_string()
|
||||||
room_alias_str = room_alias.to_string()
|
room_alias_str = room_alias.to_string()
|
||||||
|
|
||||||
if len(room_alias_str) > MAX_ALIAS_LENGTH:
|
if (
|
||||||
|
user_id not in self.hs.config.meow.validation_override
|
||||||
|
and len(room_alias_str) > MAX_ALIAS_LENGTH
|
||||||
|
):
|
||||||
raise SynapseError(
|
raise SynapseError(
|
||||||
400,
|
400,
|
||||||
"Can't create aliases longer than %s characters" % MAX_ALIAS_LENGTH,
|
"Can't create aliases longer than %s characters" % MAX_ALIAS_LENGTH,
|
||||||
|
@ -169,7 +174,7 @@ class DirectoryHandler:
|
||||||
|
|
||||||
if not self.config.roomdirectory.is_alias_creation_allowed(
|
if not self.config.roomdirectory.is_alias_creation_allowed(
|
||||||
user_id, room_id, room_alias_str
|
user_id, room_id, room_alias_str
|
||||||
):
|
) and not is_admin:
|
||||||
# Let's just return a generic message, as there may be all sorts of
|
# Let's just return a generic message, as there may be all sorts of
|
||||||
# reasons why we said no. TODO: Allow configurable error messages
|
# reasons why we said no. TODO: Allow configurable error messages
|
||||||
# per alias creation rule?
|
# per alias creation rule?
|
||||||
|
@ -505,7 +510,7 @@ class DirectoryHandler:
|
||||||
|
|
||||||
if not self.config.roomdirectory.is_publishing_room_allowed(
|
if not self.config.roomdirectory.is_publishing_room_allowed(
|
||||||
user_id, room_id, room_aliases
|
user_id, room_id, room_aliases
|
||||||
):
|
) and not await self.auth.is_server_admin(requester):
|
||||||
# Let's just return a generic message, as there may be all sorts of
|
# Let's just return a generic message, as there may be all sorts of
|
||||||
# reasons why we said no. TODO: Allow configurable error messages
|
# reasons why we said no. TODO: Allow configurable error messages
|
||||||
# per alias creation rule?
|
# per alias creation rule?
|
||||||
|
|
|
@ -53,6 +53,9 @@ if TYPE_CHECKING:
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
ONE_TIME_KEY_UPLOAD = "one_time_key_upload_lock"
|
||||||
|
|
||||||
|
|
||||||
class E2eKeysHandler:
|
class E2eKeysHandler:
|
||||||
def __init__(self, hs: "HomeServer"):
|
def __init__(self, hs: "HomeServer"):
|
||||||
self.config = hs.config
|
self.config = hs.config
|
||||||
|
@ -62,6 +65,7 @@ class E2eKeysHandler:
|
||||||
self._appservice_handler = hs.get_application_service_handler()
|
self._appservice_handler = hs.get_application_service_handler()
|
||||||
self.is_mine = hs.is_mine
|
self.is_mine = hs.is_mine
|
||||||
self.clock = hs.get_clock()
|
self.clock = hs.get_clock()
|
||||||
|
self._worker_lock_handler = hs.get_worker_locks_handler()
|
||||||
|
|
||||||
federation_registry = hs.get_federation_registry()
|
federation_registry = hs.get_federation_registry()
|
||||||
|
|
||||||
|
@ -145,6 +149,11 @@ class E2eKeysHandler:
|
||||||
remote_queries = {}
|
remote_queries = {}
|
||||||
|
|
||||||
for user_id, device_ids in device_keys_query.items():
|
for user_id, device_ids in device_keys_query.items():
|
||||||
|
if not UserID.is_valid(user_id):
|
||||||
|
# Ignore invalid user IDs, which is the same behaviour as if
|
||||||
|
# the user existed but had no keys.
|
||||||
|
continue
|
||||||
|
|
||||||
# we use UserID.from_string to catch invalid user ids
|
# we use UserID.from_string to catch invalid user ids
|
||||||
if self.is_mine(UserID.from_string(user_id)):
|
if self.is_mine(UserID.from_string(user_id)):
|
||||||
local_query[user_id] = device_ids
|
local_query[user_id] = device_ids
|
||||||
|
@ -855,45 +864,53 @@ class E2eKeysHandler:
|
||||||
async def _upload_one_time_keys_for_user(
|
async def _upload_one_time_keys_for_user(
|
||||||
self, user_id: str, device_id: str, time_now: int, one_time_keys: JsonDict
|
self, user_id: str, device_id: str, time_now: int, one_time_keys: JsonDict
|
||||||
) -> None:
|
) -> None:
|
||||||
logger.info(
|
# We take out a lock so that we don't have to worry about a client
|
||||||
"Adding one_time_keys %r for device %r for user %r at %d",
|
# sending duplicate requests.
|
||||||
one_time_keys.keys(),
|
lock_key = f"{user_id}_{device_id}"
|
||||||
device_id,
|
async with self._worker_lock_handler.acquire_lock(
|
||||||
user_id,
|
ONE_TIME_KEY_UPLOAD, lock_key
|
||||||
time_now,
|
):
|
||||||
)
|
logger.info(
|
||||||
|
"Adding one_time_keys %r for device %r for user %r at %d",
|
||||||
|
one_time_keys.keys(),
|
||||||
|
device_id,
|
||||||
|
user_id,
|
||||||
|
time_now,
|
||||||
|
)
|
||||||
|
|
||||||
# make a list of (alg, id, key) tuples
|
# make a list of (alg, id, key) tuples
|
||||||
key_list = []
|
key_list = []
|
||||||
for key_id, key_obj in one_time_keys.items():
|
for key_id, key_obj in one_time_keys.items():
|
||||||
algorithm, key_id = key_id.split(":")
|
algorithm, key_id = key_id.split(":")
|
||||||
key_list.append((algorithm, key_id, key_obj))
|
key_list.append((algorithm, key_id, key_obj))
|
||||||
|
|
||||||
# First we check if we have already persisted any of the keys.
|
# First we check if we have already persisted any of the keys.
|
||||||
existing_key_map = await self.store.get_e2e_one_time_keys(
|
existing_key_map = await self.store.get_e2e_one_time_keys(
|
||||||
user_id, device_id, [k_id for _, k_id, _ in key_list]
|
user_id, device_id, [k_id for _, k_id, _ in key_list]
|
||||||
)
|
)
|
||||||
|
|
||||||
new_keys = [] # Keys that we need to insert. (alg, id, json) tuples.
|
new_keys = [] # Keys that we need to insert. (alg, id, json) tuples.
|
||||||
for algorithm, key_id, key in key_list:
|
for algorithm, key_id, key in key_list:
|
||||||
ex_json = existing_key_map.get((algorithm, key_id), None)
|
ex_json = existing_key_map.get((algorithm, key_id), None)
|
||||||
if ex_json:
|
if ex_json:
|
||||||
if not _one_time_keys_match(ex_json, key):
|
if not _one_time_keys_match(ex_json, key):
|
||||||
raise SynapseError(
|
raise SynapseError(
|
||||||
400,
|
400,
|
||||||
(
|
(
|
||||||
"One time key %s:%s already exists. "
|
"One time key %s:%s already exists. "
|
||||||
"Old key: %s; new key: %r"
|
"Old key: %s; new key: %r"
|
||||||
|
)
|
||||||
|
% (algorithm, key_id, ex_json, key),
|
||||||
)
|
)
|
||||||
% (algorithm, key_id, ex_json, key),
|
else:
|
||||||
|
new_keys.append(
|
||||||
|
(algorithm, key_id, encode_canonical_json(key).decode("ascii"))
|
||||||
)
|
)
|
||||||
else:
|
|
||||||
new_keys.append(
|
|
||||||
(algorithm, key_id, encode_canonical_json(key).decode("ascii"))
|
|
||||||
)
|
|
||||||
|
|
||||||
log_kv({"message": "Inserting new one_time_keys.", "keys": new_keys})
|
log_kv({"message": "Inserting new one_time_keys.", "keys": new_keys})
|
||||||
await self.store.add_e2e_one_time_keys(user_id, device_id, time_now, new_keys)
|
await self.store.add_e2e_one_time_keys(
|
||||||
|
user_id, device_id, time_now, new_keys
|
||||||
|
)
|
||||||
|
|
||||||
async def upload_signing_keys_for_user(
|
async def upload_signing_keys_for_user(
|
||||||
self, user_id: str, keys: JsonDict
|
self, user_id: str, keys: JsonDict
|
||||||
|
@ -1476,6 +1493,42 @@ class E2eKeysHandler:
|
||||||
else:
|
else:
|
||||||
return exists, self.clock.time_msec() < ts_replacable_without_uia_before
|
return exists, self.clock.time_msec() < ts_replacable_without_uia_before
|
||||||
|
|
||||||
|
async def has_different_keys(self, user_id: str, body: JsonDict) -> bool:
|
||||||
|
"""
|
||||||
|
Check if a key provided in `body` differs from the same key stored in the DB. Returns
|
||||||
|
true on the first difference. If a key exists in `body` but does not exist in the DB,
|
||||||
|
returns True. If `body` has no keys, this always returns False.
|
||||||
|
Note by 'key' we mean Matrix key rather than JSON key.
|
||||||
|
|
||||||
|
The purpose of this function is to detect whether or not we need to apply UIA checks.
|
||||||
|
We must apply UIA checks if any key in the database is being overwritten. If a key is
|
||||||
|
being inserted for the first time, or if the key exactly matches what is in the database,
|
||||||
|
then no UIA check needs to be performed.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
user_id: The user who sent the `body`.
|
||||||
|
body: The JSON request body from POST /keys/device_signing/upload
|
||||||
|
Returns:
|
||||||
|
True if any key in `body` has a different value in the database.
|
||||||
|
"""
|
||||||
|
# Ensure that each key provided in the request body exactly matches the one we have stored.
|
||||||
|
# The first time we see the DB having a different key to the matching request key, bail.
|
||||||
|
# Note: we do not care if the DB has a key which the request does not specify, as we only
|
||||||
|
# care about *replacements* or *insertions* (i.e UPSERT)
|
||||||
|
req_body_key_to_db_key = {
|
||||||
|
"master_key": "master",
|
||||||
|
"self_signing_key": "self_signing",
|
||||||
|
"user_signing_key": "user_signing",
|
||||||
|
}
|
||||||
|
for req_body_key, db_key in req_body_key_to_db_key.items():
|
||||||
|
if req_body_key in body:
|
||||||
|
existing_key = await self.store.get_e2e_cross_signing_key(
|
||||||
|
user_id, db_key
|
||||||
|
)
|
||||||
|
if existing_key != body[req_body_key]:
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
def _check_cross_signing_key(
|
def _check_cross_signing_key(
|
||||||
key: JsonDict, user_id: str, key_type: str, signing_key: Optional[VerifyKey] = None
|
key: JsonDict, user_id: str, key_type: str, signing_key: Optional[VerifyKey] = None
|
||||||
|
|
|
@ -148,6 +148,7 @@ class EventHandler:
|
||||||
def __init__(self, hs: "HomeServer"):
|
def __init__(self, hs: "HomeServer"):
|
||||||
self.store = hs.get_datastores().main
|
self.store = hs.get_datastores().main
|
||||||
self._storage_controllers = hs.get_storage_controllers()
|
self._storage_controllers = hs.get_storage_controllers()
|
||||||
|
self._config = hs.config
|
||||||
|
|
||||||
async def get_event(
|
async def get_event(
|
||||||
self,
|
self,
|
||||||
|
@ -189,7 +190,11 @@ class EventHandler:
|
||||||
is_peeking = not is_user_in_room
|
is_peeking = not is_user_in_room
|
||||||
|
|
||||||
filtered = await filter_events_for_client(
|
filtered = await filter_events_for_client(
|
||||||
self._storage_controllers, user.to_string(), [event], is_peeking=is_peeking
|
self._storage_controllers,
|
||||||
|
user.to_string(),
|
||||||
|
[event],
|
||||||
|
is_peeking=is_peeking,
|
||||||
|
msc4115_membership_on_events=self._config.experimental.msc4115_membership_on_events,
|
||||||
)
|
)
|
||||||
|
|
||||||
if not filtered:
|
if not filtered:
|
||||||
|
|
|
@ -1452,7 +1452,7 @@ class FederationHandler:
|
||||||
room_version_obj, event_dict
|
room_version_obj, event_dict
|
||||||
)
|
)
|
||||||
|
|
||||||
EventValidator().validate_builder(builder)
|
EventValidator().validate_builder(builder, self.hs.config)
|
||||||
|
|
||||||
# Try several times, it could fail with PartialStateConflictError
|
# Try several times, it could fail with PartialStateConflictError
|
||||||
# in send_membership_event, cf comment in except block.
|
# in send_membership_event, cf comment in except block.
|
||||||
|
@ -1617,7 +1617,7 @@ class FederationHandler:
|
||||||
builder = self.event_builder_factory.for_room_version(
|
builder = self.event_builder_factory.for_room_version(
|
||||||
room_version_obj, event_dict
|
room_version_obj, event_dict
|
||||||
)
|
)
|
||||||
EventValidator().validate_builder(builder)
|
EventValidator().validate_builder(builder, self.hs.config)
|
||||||
|
|
||||||
(
|
(
|
||||||
event,
|
event,
|
||||||
|
|
|
@ -221,7 +221,10 @@ class InitialSyncHandler:
|
||||||
).addErrback(unwrapFirstError)
|
).addErrback(unwrapFirstError)
|
||||||
|
|
||||||
messages = await filter_events_for_client(
|
messages = await filter_events_for_client(
|
||||||
self._storage_controllers, user_id, messages
|
self._storage_controllers,
|
||||||
|
user_id,
|
||||||
|
messages,
|
||||||
|
msc4115_membership_on_events=self.hs.config.experimental.msc4115_membership_on_events,
|
||||||
)
|
)
|
||||||
|
|
||||||
start_token = now_token.copy_and_replace(StreamKeyType.ROOM, token)
|
start_token = now_token.copy_and_replace(StreamKeyType.ROOM, token)
|
||||||
|
@ -380,6 +383,7 @@ class InitialSyncHandler:
|
||||||
requester.user.to_string(),
|
requester.user.to_string(),
|
||||||
messages,
|
messages,
|
||||||
is_peeking=is_peeking,
|
is_peeking=is_peeking,
|
||||||
|
msc4115_membership_on_events=self.hs.config.experimental.msc4115_membership_on_events,
|
||||||
)
|
)
|
||||||
|
|
||||||
start_token = StreamToken.START.copy_and_replace(StreamKeyType.ROOM, token)
|
start_token = StreamToken.START.copy_and_replace(StreamKeyType.ROOM, token)
|
||||||
|
@ -494,6 +498,7 @@ class InitialSyncHandler:
|
||||||
requester.user.to_string(),
|
requester.user.to_string(),
|
||||||
messages,
|
messages,
|
||||||
is_peeking=is_peeking,
|
is_peeking=is_peeking,
|
||||||
|
msc4115_membership_on_events=self.hs.config.experimental.msc4115_membership_on_events,
|
||||||
)
|
)
|
||||||
|
|
||||||
start_token = now_token.copy_and_replace(StreamKeyType.ROOM, token)
|
start_token = now_token.copy_and_replace(StreamKeyType.ROOM, token)
|
||||||
|
|
|
@ -496,13 +496,6 @@ class EventCreationHandler:
|
||||||
|
|
||||||
self.room_prejoin_state_types = self.hs.config.api.room_prejoin_state
|
self.room_prejoin_state_types = self.hs.config.api.room_prejoin_state
|
||||||
|
|
||||||
self.membership_types_to_include_profile_data_in = {
|
|
||||||
Membership.JOIN,
|
|
||||||
Membership.KNOCK,
|
|
||||||
}
|
|
||||||
if self.hs.config.server.include_profile_data_on_invite:
|
|
||||||
self.membership_types_to_include_profile_data_in.add(Membership.INVITE)
|
|
||||||
|
|
||||||
self.send_event = ReplicationSendEventRestServlet.make_client(hs)
|
self.send_event = ReplicationSendEventRestServlet.make_client(hs)
|
||||||
self.send_events = ReplicationSendEventsRestServlet.make_client(hs)
|
self.send_events = ReplicationSendEventsRestServlet.make_client(hs)
|
||||||
|
|
||||||
|
@ -594,8 +587,6 @@ class EventCreationHandler:
|
||||||
Creates an FrozenEvent object, filling out auth_events, prev_events,
|
Creates an FrozenEvent object, filling out auth_events, prev_events,
|
||||||
etc.
|
etc.
|
||||||
|
|
||||||
Adds display names to Join membership events.
|
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
requester
|
requester
|
||||||
event_dict: An entire event
|
event_dict: An entire event
|
||||||
|
@ -670,30 +661,7 @@ class EventCreationHandler:
|
||||||
room_version_obj, event_dict
|
room_version_obj, event_dict
|
||||||
)
|
)
|
||||||
|
|
||||||
self.validator.validate_builder(builder)
|
self.validator.validate_builder(builder, self.config)
|
||||||
|
|
||||||
if builder.type == EventTypes.Member:
|
|
||||||
membership = builder.content.get("membership", None)
|
|
||||||
target = UserID.from_string(builder.state_key)
|
|
||||||
|
|
||||||
if membership in self.membership_types_to_include_profile_data_in:
|
|
||||||
# If event doesn't include a display name, add one.
|
|
||||||
profile = self.profile_handler
|
|
||||||
content = builder.content
|
|
||||||
|
|
||||||
try:
|
|
||||||
if "displayname" not in content:
|
|
||||||
displayname = await profile.get_displayname(target)
|
|
||||||
if displayname is not None:
|
|
||||||
content["displayname"] = displayname
|
|
||||||
if "avatar_url" not in content:
|
|
||||||
avatar_url = await profile.get_avatar_url(target)
|
|
||||||
if avatar_url is not None:
|
|
||||||
content["avatar_url"] = avatar_url
|
|
||||||
except Exception as e:
|
|
||||||
logger.info(
|
|
||||||
"Failed to get profile information for %r: %s", target, e
|
|
||||||
)
|
|
||||||
|
|
||||||
is_exempt = await self._is_exempt_from_privacy_policy(builder, requester)
|
is_exempt = await self._is_exempt_from_privacy_policy(builder, requester)
|
||||||
if require_consent and not is_exempt:
|
if require_consent and not is_exempt:
|
||||||
|
@ -1352,6 +1320,8 @@ class EventCreationHandler:
|
||||||
Raises:
|
Raises:
|
||||||
SynapseError if the event is invalid.
|
SynapseError if the event is invalid.
|
||||||
"""
|
"""
|
||||||
|
if event.sender in self.config.meow.validation_override:
|
||||||
|
return
|
||||||
|
|
||||||
relation = relation_from_event(event)
|
relation = relation_from_event(event)
|
||||||
if not relation:
|
if not relation:
|
||||||
|
@ -1770,7 +1740,8 @@ class EventCreationHandler:
|
||||||
|
|
||||||
await self._maybe_kick_guest_users(event, context)
|
await self._maybe_kick_guest_users(event, context)
|
||||||
|
|
||||||
if event.type == EventTypes.CanonicalAlias:
|
validation_override = event.sender in self.config.meow.validation_override
|
||||||
|
if event.type == EventTypes.CanonicalAlias and not validation_override:
|
||||||
# Validate a newly added alias or newly added alt_aliases.
|
# Validate a newly added alias or newly added alt_aliases.
|
||||||
|
|
||||||
original_alias = None
|
original_alias = None
|
||||||
|
@ -2125,7 +2096,7 @@ class EventCreationHandler:
|
||||||
builder = self.event_builder_factory.for_room_version(
|
builder = self.event_builder_factory.for_room_version(
|
||||||
original_event.room_version, third_party_result
|
original_event.room_version, third_party_result
|
||||||
)
|
)
|
||||||
self.validator.validate_builder(builder)
|
self.validator.validate_builder(builder, self.config)
|
||||||
except SynapseError as e:
|
except SynapseError as e:
|
||||||
raise Exception(
|
raise Exception(
|
||||||
"Third party rules module created an invalid event: " + e.msg,
|
"Third party rules module created an invalid event: " + e.msg,
|
||||||
|
|
|
@ -623,6 +623,7 @@ class PaginationHandler:
|
||||||
user_id,
|
user_id,
|
||||||
events,
|
events,
|
||||||
is_peeking=(member_event_id is None),
|
is_peeking=(member_event_id is None),
|
||||||
|
msc4115_membership_on_events=self.hs.config.experimental.msc4115_membership_on_events,
|
||||||
)
|
)
|
||||||
|
|
||||||
# if after the filter applied there are no more events
|
# if after the filter applied there are no more events
|
||||||
|
|
|
@ -20,7 +20,7 @@
|
||||||
#
|
#
|
||||||
import logging
|
import logging
|
||||||
import random
|
import random
|
||||||
from typing import TYPE_CHECKING, Optional, Union
|
from typing import TYPE_CHECKING, List, Optional, Union
|
||||||
|
|
||||||
from synapse.api.errors import (
|
from synapse.api.errors import (
|
||||||
AuthError,
|
AuthError,
|
||||||
|
@ -64,8 +64,10 @@ class ProfileHandler:
|
||||||
self.user_directory_handler = hs.get_user_directory_handler()
|
self.user_directory_handler = hs.get_user_directory_handler()
|
||||||
self.request_ratelimiter = hs.get_request_ratelimiter()
|
self.request_ratelimiter = hs.get_request_ratelimiter()
|
||||||
|
|
||||||
self.max_avatar_size = hs.config.server.max_avatar_size
|
self.max_avatar_size: Optional[int] = hs.config.server.max_avatar_size
|
||||||
self.allowed_avatar_mimetypes = hs.config.server.allowed_avatar_mimetypes
|
self.allowed_avatar_mimetypes: Optional[List[str]] = (
|
||||||
|
hs.config.server.allowed_avatar_mimetypes
|
||||||
|
)
|
||||||
|
|
||||||
self._is_mine_server_name = hs.is_mine_server_name
|
self._is_mine_server_name = hs.is_mine_server_name
|
||||||
|
|
||||||
|
@ -337,6 +339,12 @@ class ProfileHandler:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
if self.max_avatar_size:
|
if self.max_avatar_size:
|
||||||
|
if media_info.media_length is None:
|
||||||
|
logger.warning(
|
||||||
|
"Forbidding avatar change to %s: unknown media size",
|
||||||
|
mxc,
|
||||||
|
)
|
||||||
|
return False
|
||||||
# Ensure avatar does not exceed max allowed avatar size
|
# Ensure avatar does not exceed max allowed avatar size
|
||||||
if media_info.media_length > self.max_avatar_size:
|
if media_info.media_length > self.max_avatar_size:
|
||||||
logger.warning(
|
logger.warning(
|
||||||
|
|
|
@ -20,11 +20,12 @@
|
||||||
#
|
#
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
from typing import TYPE_CHECKING
|
from typing import TYPE_CHECKING, Optional
|
||||||
|
|
||||||
from synapse.api.constants import ReceiptTypes
|
from synapse.api.constants import ReceiptTypes
|
||||||
from synapse.api.errors import SynapseError
|
from synapse.api.errors import SynapseError
|
||||||
from synapse.util.async_helpers import Linearizer
|
from synapse.util.async_helpers import Linearizer
|
||||||
|
from synapse.types import JsonDict
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from synapse.server import HomeServer
|
from synapse.server import HomeServer
|
||||||
|
@ -39,7 +40,11 @@ class ReadMarkerHandler:
|
||||||
self.read_marker_linearizer = Linearizer(name="read_marker")
|
self.read_marker_linearizer = Linearizer(name="read_marker")
|
||||||
|
|
||||||
async def received_client_read_marker(
|
async def received_client_read_marker(
|
||||||
self, room_id: str, user_id: str, event_id: str
|
self,
|
||||||
|
room_id: str,
|
||||||
|
user_id: str,
|
||||||
|
event_id: str,
|
||||||
|
extra_content: Optional[JsonDict] = None,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Updates the read marker for a given user in a given room if the event ID given
|
"""Updates the read marker for a given user in a given room if the event ID given
|
||||||
is ahead in the stream relative to the current read marker.
|
is ahead in the stream relative to the current read marker.
|
||||||
|
@ -71,7 +76,7 @@ class ReadMarkerHandler:
|
||||||
should_update = event_ordering > old_event_ordering
|
should_update = event_ordering > old_event_ordering
|
||||||
|
|
||||||
if should_update:
|
if should_update:
|
||||||
content = {"event_id": event_id}
|
content = {"event_id": event_id, **(extra_content or {})}
|
||||||
await self.account_data_handler.add_account_data_to_room(
|
await self.account_data_handler.add_account_data_to_room(
|
||||||
user_id, room_id, ReceiptTypes.FULLY_READ, content
|
user_id, room_id, ReceiptTypes.FULLY_READ, content
|
||||||
)
|
)
|
||||||
|
|
|
@ -181,6 +181,7 @@ class ReceiptsHandler:
|
||||||
user_id: UserID,
|
user_id: UserID,
|
||||||
event_id: str,
|
event_id: str,
|
||||||
thread_id: Optional[str],
|
thread_id: Optional[str],
|
||||||
|
extra_content: Optional[JsonDict] = None,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Called when a client tells us a local user has read up to the given
|
"""Called when a client tells us a local user has read up to the given
|
||||||
event_id in the room.
|
event_id in the room.
|
||||||
|
@ -197,7 +198,7 @@ class ReceiptsHandler:
|
||||||
user_id=user_id.to_string(),
|
user_id=user_id.to_string(),
|
||||||
event_ids=[event_id],
|
event_ids=[event_id],
|
||||||
thread_id=thread_id,
|
thread_id=thread_id,
|
||||||
data={"ts": int(self.clock.time_msec())},
|
data={"ts": int(self.clock.time_msec()), **(extra_content or {})},
|
||||||
)
|
)
|
||||||
|
|
||||||
is_new = await self._handle_new_receipts([receipt])
|
is_new = await self._handle_new_receipts([receipt])
|
||||||
|
|
|
@ -148,22 +148,25 @@ class RegistrationHandler:
|
||||||
localpart: str,
|
localpart: str,
|
||||||
guest_access_token: Optional[str] = None,
|
guest_access_token: Optional[str] = None,
|
||||||
assigned_user_id: Optional[str] = None,
|
assigned_user_id: Optional[str] = None,
|
||||||
|
allow_invalid: bool = False,
|
||||||
inhibit_user_in_use_error: bool = False,
|
inhibit_user_in_use_error: bool = False,
|
||||||
) -> None:
|
) -> None:
|
||||||
if types.contains_invalid_mxid_characters(localpart):
|
# meow: allow admins to register invalid user ids
|
||||||
raise SynapseError(
|
if not allow_invalid:
|
||||||
400,
|
if types.contains_invalid_mxid_characters(localpart):
|
||||||
"User ID can only contain characters a-z, 0-9, or '=_-./+'",
|
raise SynapseError(
|
||||||
Codes.INVALID_USERNAME,
|
400,
|
||||||
)
|
"User ID can only contain characters a-z, 0-9, or '=_-./+'",
|
||||||
|
Codes.INVALID_USERNAME,
|
||||||
|
)
|
||||||
|
|
||||||
if not localpart:
|
if not localpart:
|
||||||
raise SynapseError(400, "User ID cannot be empty", Codes.INVALID_USERNAME)
|
raise SynapseError(400, "User ID cannot be empty", Codes.INVALID_USERNAME)
|
||||||
|
|
||||||
if localpart[0] == "_":
|
if localpart[0] == "_":
|
||||||
raise SynapseError(
|
raise SynapseError(
|
||||||
400, "User ID may not begin with _", Codes.INVALID_USERNAME
|
400, "User ID may not begin with _", Codes.INVALID_USERNAME
|
||||||
)
|
)
|
||||||
|
|
||||||
user = UserID(localpart, self.hs.hostname)
|
user = UserID(localpart, self.hs.hostname)
|
||||||
user_id = user.to_string()
|
user_id = user.to_string()
|
||||||
|
@ -177,14 +180,16 @@ class RegistrationHandler:
|
||||||
"A different user ID has already been registered for this session",
|
"A different user ID has already been registered for this session",
|
||||||
)
|
)
|
||||||
|
|
||||||
self.check_user_id_not_appservice_exclusive(user_id)
|
# meow: allow admins to register reserved user ids and long user ids
|
||||||
|
if not allow_invalid:
|
||||||
|
self.check_user_id_not_appservice_exclusive(user_id)
|
||||||
|
|
||||||
if len(user_id) > MAX_USERID_LENGTH:
|
if len(user_id) > MAX_USERID_LENGTH:
|
||||||
raise SynapseError(
|
raise SynapseError(
|
||||||
400,
|
400,
|
||||||
"User ID may not be longer than %s characters" % (MAX_USERID_LENGTH,),
|
"User ID may not be longer than %s characters" % (MAX_USERID_LENGTH,),
|
||||||
Codes.INVALID_USERNAME,
|
Codes.INVALID_USERNAME,
|
||||||
)
|
)
|
||||||
|
|
||||||
users = await self.store.get_users_by_id_case_insensitive(user_id)
|
users = await self.store.get_users_by_id_case_insensitive(user_id)
|
||||||
if users:
|
if users:
|
||||||
|
@ -290,7 +295,12 @@ class RegistrationHandler:
|
||||||
await self.auth_blocking.check_auth_blocking(threepid=threepid)
|
await self.auth_blocking.check_auth_blocking(threepid=threepid)
|
||||||
|
|
||||||
if localpart is not None:
|
if localpart is not None:
|
||||||
await self.check_username(localpart, guest_access_token=guest_access_token)
|
allow_invalid = by_admin and self.hs.config.meow.admin_api_register_invalid
|
||||||
|
await self.check_username(
|
||||||
|
localpart,
|
||||||
|
guest_access_token=guest_access_token,
|
||||||
|
allow_invalid=allow_invalid,
|
||||||
|
)
|
||||||
|
|
||||||
was_guest = guest_access_token is not None
|
was_guest = guest_access_token is not None
|
||||||
|
|
||||||
|
@ -590,7 +600,7 @@ class RegistrationHandler:
|
||||||
# moving away from bare excepts is a good thing to do.
|
# moving away from bare excepts is a good thing to do.
|
||||||
logger.error("Failed to join new user to %r: %r", r, e)
|
logger.error("Failed to join new user to %r: %r", r, e)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error("Failed to join new user to %r: %r", r, e)
|
logger.error("Failed to join new user to %r: %r", r, e, exc_info=True)
|
||||||
|
|
||||||
async def _auto_join_rooms(self, user_id: str) -> None:
|
async def _auto_join_rooms(self, user_id: str) -> None:
|
||||||
"""Automatically joins users to auto join rooms - creating the room in the first place
|
"""Automatically joins users to auto join rooms - creating the room in the first place
|
||||||
|
|
|
@ -95,6 +95,7 @@ class RelationsHandler:
|
||||||
self._event_handler = hs.get_event_handler()
|
self._event_handler = hs.get_event_handler()
|
||||||
self._event_serializer = hs.get_event_client_serializer()
|
self._event_serializer = hs.get_event_client_serializer()
|
||||||
self._event_creation_handler = hs.get_event_creation_handler()
|
self._event_creation_handler = hs.get_event_creation_handler()
|
||||||
|
self._config = hs.config
|
||||||
|
|
||||||
async def get_relations(
|
async def get_relations(
|
||||||
self,
|
self,
|
||||||
|
@ -163,6 +164,7 @@ class RelationsHandler:
|
||||||
user_id,
|
user_id,
|
||||||
events,
|
events,
|
||||||
is_peeking=(member_event_id is None),
|
is_peeking=(member_event_id is None),
|
||||||
|
msc4115_membership_on_events=self._config.experimental.msc4115_membership_on_events,
|
||||||
)
|
)
|
||||||
|
|
||||||
# The relations returned for the requested event do include their
|
# The relations returned for the requested event do include their
|
||||||
|
@ -391,9 +393,9 @@ class RelationsHandler:
|
||||||
|
|
||||||
# Attempt to find another event to use as the latest event.
|
# Attempt to find another event to use as the latest event.
|
||||||
potential_events, _ = await self._main_store.get_relations_for_event(
|
potential_events, _ = await self._main_store.get_relations_for_event(
|
||||||
|
room_id,
|
||||||
event_id,
|
event_id,
|
||||||
event,
|
event,
|
||||||
room_id,
|
|
||||||
RelationTypes.THREAD,
|
RelationTypes.THREAD,
|
||||||
direction=Direction.FORWARDS,
|
direction=Direction.FORWARDS,
|
||||||
)
|
)
|
||||||
|
@ -608,6 +610,7 @@ class RelationsHandler:
|
||||||
user_id,
|
user_id,
|
||||||
events,
|
events,
|
||||||
is_peeking=(member_event_id is None),
|
is_peeking=(member_event_id is None),
|
||||||
|
msc4115_membership_on_events=self._config.experimental.msc4115_membership_on_events,
|
||||||
)
|
)
|
||||||
|
|
||||||
aggregations = await self.get_bundled_aggregations(
|
aggregations = await self.get_bundled_aggregations(
|
||||||
|
|
|
@ -893,11 +893,23 @@ class RoomCreationHandler:
|
||||||
|
|
||||||
self._validate_room_config(config, visibility)
|
self._validate_room_config(config, visibility)
|
||||||
|
|
||||||
room_id = await self._generate_and_create_room_id(
|
if "room_id" in config:
|
||||||
creator_id=user_id,
|
room_id = config["room_id"]
|
||||||
is_public=is_public,
|
try:
|
||||||
room_version=room_version,
|
await self.store.store_room(
|
||||||
)
|
room_id=room_id,
|
||||||
|
room_creator_user_id=user_id,
|
||||||
|
is_public=is_public,
|
||||||
|
room_version=room_version,
|
||||||
|
)
|
||||||
|
except StoreError:
|
||||||
|
raise SynapseError(409, "Room ID already in use", errcode="M_CONFLICT")
|
||||||
|
else:
|
||||||
|
room_id = await self._generate_and_create_room_id(
|
||||||
|
creator_id=user_id,
|
||||||
|
is_public=is_public,
|
||||||
|
room_version=room_version,
|
||||||
|
)
|
||||||
|
|
||||||
# Check whether this visibility value is blocked by a third party module
|
# Check whether this visibility value is blocked by a third party module
|
||||||
allowed_by_third_party_rules = (
|
allowed_by_third_party_rules = (
|
||||||
|
@ -916,7 +928,7 @@ class RoomCreationHandler:
|
||||||
room_aliases.append(room_alias.to_string())
|
room_aliases.append(room_alias.to_string())
|
||||||
if not self.config.roomdirectory.is_publishing_room_allowed(
|
if not self.config.roomdirectory.is_publishing_room_allowed(
|
||||||
user_id, room_id, room_aliases
|
user_id, room_id, room_aliases
|
||||||
):
|
) and not is_requester_admin:
|
||||||
# allow room creation to continue but do not publish room
|
# allow room creation to continue but do not publish room
|
||||||
await self.store.set_room_is_public(room_id, False)
|
await self.store.set_room_is_public(room_id, False)
|
||||||
|
|
||||||
|
@ -1189,7 +1201,7 @@ class RoomCreationHandler:
|
||||||
events_to_send.append((power_event, power_context))
|
events_to_send.append((power_event, power_context))
|
||||||
else:
|
else:
|
||||||
power_level_content: JsonDict = {
|
power_level_content: JsonDict = {
|
||||||
"users": {creator_id: 100},
|
"users": {creator_id: 9001},
|
||||||
"users_default": 0,
|
"users_default": 0,
|
||||||
"events": {
|
"events": {
|
||||||
EventTypes.Name: 50,
|
EventTypes.Name: 50,
|
||||||
|
@ -1476,6 +1488,7 @@ class RoomContextHandler:
|
||||||
user.to_string(),
|
user.to_string(),
|
||||||
events,
|
events,
|
||||||
is_peeking=is_peeking,
|
is_peeking=is_peeking,
|
||||||
|
msc4115_membership_on_events=self.hs.config.experimental.msc4115_membership_on_events,
|
||||||
)
|
)
|
||||||
|
|
||||||
event = await self.store.get_event(
|
event = await self.store.get_event(
|
||||||
|
|
|
@ -106,6 +106,13 @@ class RoomMemberHandler(metaclass=abc.ABCMeta):
|
||||||
self.event_auth_handler = hs.get_event_auth_handler()
|
self.event_auth_handler = hs.get_event_auth_handler()
|
||||||
self._worker_lock_handler = hs.get_worker_locks_handler()
|
self._worker_lock_handler = hs.get_worker_locks_handler()
|
||||||
|
|
||||||
|
self._membership_types_to_include_profile_data_in = {
|
||||||
|
Membership.JOIN,
|
||||||
|
Membership.KNOCK,
|
||||||
|
}
|
||||||
|
if self.hs.config.server.include_profile_data_on_invite:
|
||||||
|
self._membership_types_to_include_profile_data_in.add(Membership.INVITE)
|
||||||
|
|
||||||
self.member_linearizer: Linearizer = Linearizer(name="member")
|
self.member_linearizer: Linearizer = Linearizer(name="member")
|
||||||
self.member_as_limiter = Linearizer(max_count=10, name="member_as_limiter")
|
self.member_as_limiter = Linearizer(max_count=10, name="member_as_limiter")
|
||||||
|
|
||||||
|
@ -752,35 +759,44 @@ class RoomMemberHandler(metaclass=abc.ABCMeta):
|
||||||
and requester.user.to_string() == self._server_notices_mxid
|
and requester.user.to_string() == self._server_notices_mxid
|
||||||
)
|
)
|
||||||
|
|
||||||
|
requester_suspended = await self.store.get_user_suspended_status(
|
||||||
|
requester.user.to_string()
|
||||||
|
)
|
||||||
|
if action == Membership.INVITE and requester_suspended:
|
||||||
|
raise SynapseError(
|
||||||
|
403,
|
||||||
|
"Sending invites while account is suspended is not allowed.",
|
||||||
|
Codes.USER_ACCOUNT_SUSPENDED,
|
||||||
|
)
|
||||||
|
|
||||||
|
if target.to_string() != requester.user.to_string():
|
||||||
|
target_suspended = await self.store.get_user_suspended_status(
|
||||||
|
target.to_string()
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
target_suspended = requester_suspended
|
||||||
|
|
||||||
|
if action == Membership.JOIN and target_suspended:
|
||||||
|
raise SynapseError(
|
||||||
|
403,
|
||||||
|
"Joining rooms while account is suspended is not allowed.",
|
||||||
|
Codes.USER_ACCOUNT_SUSPENDED,
|
||||||
|
)
|
||||||
|
if action == Membership.KNOCK and target_suspended:
|
||||||
|
raise SynapseError(
|
||||||
|
403,
|
||||||
|
"Knocking on rooms while account is suspended is not allowed.",
|
||||||
|
Codes.USER_ACCOUNT_SUSPENDED,
|
||||||
|
)
|
||||||
|
|
||||||
if (
|
if (
|
||||||
not self.allow_per_room_profiles and not is_requester_server_notices_user
|
not self.allow_per_room_profiles and not is_requester_server_notices_user
|
||||||
) or requester.shadow_banned:
|
) or requester.shadow_banned:
|
||||||
# Strip profile data, knowing that new profile data will be added to the
|
# Strip profile data, knowing that new profile data will be added to
|
||||||
# event's content in event_creation_handler.create_event() using the target's
|
# the event's content below using the target's global profile.
|
||||||
# global profile.
|
|
||||||
content.pop("displayname", None)
|
content.pop("displayname", None)
|
||||||
content.pop("avatar_url", None)
|
content.pop("avatar_url", None)
|
||||||
|
|
||||||
if len(content.get("displayname") or "") > MAX_DISPLAYNAME_LEN:
|
|
||||||
raise SynapseError(
|
|
||||||
400,
|
|
||||||
f"Displayname is too long (max {MAX_DISPLAYNAME_LEN})",
|
|
||||||
errcode=Codes.BAD_JSON,
|
|
||||||
)
|
|
||||||
|
|
||||||
if len(content.get("avatar_url") or "") > MAX_AVATAR_URL_LEN:
|
|
||||||
raise SynapseError(
|
|
||||||
400,
|
|
||||||
f"Avatar URL is too long (max {MAX_AVATAR_URL_LEN})",
|
|
||||||
errcode=Codes.BAD_JSON,
|
|
||||||
)
|
|
||||||
|
|
||||||
if "avatar_url" in content and content.get("avatar_url") is not None:
|
|
||||||
if not await self.profile_handler.check_avatar_size_and_mime_type(
|
|
||||||
content["avatar_url"],
|
|
||||||
):
|
|
||||||
raise SynapseError(403, "This avatar is not allowed", Codes.FORBIDDEN)
|
|
||||||
|
|
||||||
# The event content should *not* include the authorising user as
|
# The event content should *not* include the authorising user as
|
||||||
# it won't be properly signed. Strip it out since it might come
|
# it won't be properly signed. Strip it out since it might come
|
||||||
# back from a client updating a display name / avatar.
|
# back from a client updating a display name / avatar.
|
||||||
|
@ -793,6 +809,29 @@ class RoomMemberHandler(metaclass=abc.ABCMeta):
|
||||||
if action in ["kick", "unban"]:
|
if action in ["kick", "unban"]:
|
||||||
effective_membership_state = "leave"
|
effective_membership_state = "leave"
|
||||||
|
|
||||||
|
if effective_membership_state not in Membership.LIST:
|
||||||
|
raise SynapseError(400, "Invalid membership key")
|
||||||
|
|
||||||
|
# Add profile data for joins etc, if no per-room profile.
|
||||||
|
if (
|
||||||
|
effective_membership_state
|
||||||
|
in self._membership_types_to_include_profile_data_in
|
||||||
|
):
|
||||||
|
# If event doesn't include a display name, add one.
|
||||||
|
profile = self.profile_handler
|
||||||
|
|
||||||
|
try:
|
||||||
|
if "displayname" not in content:
|
||||||
|
displayname = await profile.get_displayname(target)
|
||||||
|
if displayname is not None:
|
||||||
|
content["displayname"] = displayname
|
||||||
|
if "avatar_url" not in content:
|
||||||
|
avatar_url = await profile.get_avatar_url(target)
|
||||||
|
if avatar_url is not None:
|
||||||
|
content["avatar_url"] = avatar_url
|
||||||
|
except Exception as e:
|
||||||
|
logger.info("Failed to get profile information for %r: %s", target, e)
|
||||||
|
|
||||||
# if this is a join with a 3pid signature, we may need to turn a 3pid
|
# if this is a join with a 3pid signature, we may need to turn a 3pid
|
||||||
# invite into a normal invite before we can handle the join.
|
# invite into a normal invite before we can handle the join.
|
||||||
if third_party_signed is not None:
|
if third_party_signed is not None:
|
||||||
|
|
|
@ -480,7 +480,10 @@ class SearchHandler:
|
||||||
filtered_events = await search_filter.filter([r["event"] for r in results])
|
filtered_events = await search_filter.filter([r["event"] for r in results])
|
||||||
|
|
||||||
events = await filter_events_for_client(
|
events = await filter_events_for_client(
|
||||||
self._storage_controllers, user.to_string(), filtered_events
|
self._storage_controllers,
|
||||||
|
user.to_string(),
|
||||||
|
filtered_events,
|
||||||
|
msc4115_membership_on_events=self.hs.config.experimental.msc4115_membership_on_events,
|
||||||
)
|
)
|
||||||
|
|
||||||
events.sort(key=lambda e: -rank_map[e.event_id])
|
events.sort(key=lambda e: -rank_map[e.event_id])
|
||||||
|
@ -579,7 +582,10 @@ class SearchHandler:
|
||||||
filtered_events = await search_filter.filter([r["event"] for r in results])
|
filtered_events = await search_filter.filter([r["event"] for r in results])
|
||||||
|
|
||||||
events = await filter_events_for_client(
|
events = await filter_events_for_client(
|
||||||
self._storage_controllers, user.to_string(), filtered_events
|
self._storage_controllers,
|
||||||
|
user.to_string(),
|
||||||
|
filtered_events,
|
||||||
|
msc4115_membership_on_events=self.hs.config.experimental.msc4115_membership_on_events,
|
||||||
)
|
)
|
||||||
|
|
||||||
room_events.extend(events)
|
room_events.extend(events)
|
||||||
|
@ -664,11 +670,17 @@ class SearchHandler:
|
||||||
)
|
)
|
||||||
|
|
||||||
events_before = await filter_events_for_client(
|
events_before = await filter_events_for_client(
|
||||||
self._storage_controllers, user.to_string(), res.events_before
|
self._storage_controllers,
|
||||||
|
user.to_string(),
|
||||||
|
res.events_before,
|
||||||
|
msc4115_membership_on_events=self.hs.config.experimental.msc4115_membership_on_events,
|
||||||
)
|
)
|
||||||
|
|
||||||
events_after = await filter_events_for_client(
|
events_after = await filter_events_for_client(
|
||||||
self._storage_controllers, user.to_string(), res.events_after
|
self._storage_controllers,
|
||||||
|
user.to_string(),
|
||||||
|
res.events_after,
|
||||||
|
msc4115_membership_on_events=self.hs.config.experimental.msc4115_membership_on_events,
|
||||||
)
|
)
|
||||||
|
|
||||||
context: JsonDict = {
|
context: JsonDict = {
|
||||||
|
|
|
@ -169,6 +169,7 @@ class UsernameMappingSession:
|
||||||
# attributes returned by the ID mapper
|
# attributes returned by the ID mapper
|
||||||
display_name: Optional[str]
|
display_name: Optional[str]
|
||||||
emails: StrCollection
|
emails: StrCollection
|
||||||
|
avatar_url: Optional[str]
|
||||||
|
|
||||||
# An optional dictionary of extra attributes to be provided to the client in the
|
# An optional dictionary of extra attributes to be provided to the client in the
|
||||||
# login response.
|
# login response.
|
||||||
|
@ -183,6 +184,7 @@ class UsernameMappingSession:
|
||||||
# choices made by the user
|
# choices made by the user
|
||||||
chosen_localpart: Optional[str] = None
|
chosen_localpart: Optional[str] = None
|
||||||
use_display_name: bool = True
|
use_display_name: bool = True
|
||||||
|
use_avatar: bool = True
|
||||||
emails_to_use: StrCollection = ()
|
emails_to_use: StrCollection = ()
|
||||||
terms_accepted_version: Optional[str] = None
|
terms_accepted_version: Optional[str] = None
|
||||||
|
|
||||||
|
@ -660,6 +662,9 @@ class SsoHandler:
|
||||||
remote_user_id=remote_user_id,
|
remote_user_id=remote_user_id,
|
||||||
display_name=attributes.display_name,
|
display_name=attributes.display_name,
|
||||||
emails=attributes.emails,
|
emails=attributes.emails,
|
||||||
|
avatar_url=attributes.picture,
|
||||||
|
# Default to using all mapped emails. Will be overwritten in handle_submit_username_request.
|
||||||
|
emails_to_use=attributes.emails,
|
||||||
client_redirect_url=client_redirect_url,
|
client_redirect_url=client_redirect_url,
|
||||||
expiry_time_ms=now + self._MAPPING_SESSION_VALIDITY_PERIOD_MS,
|
expiry_time_ms=now + self._MAPPING_SESSION_VALIDITY_PERIOD_MS,
|
||||||
extra_login_attributes=extra_login_attributes,
|
extra_login_attributes=extra_login_attributes,
|
||||||
|
@ -812,7 +817,7 @@ class SsoHandler:
|
||||||
server_name = profile["avatar_url"].split("/")[-2]
|
server_name = profile["avatar_url"].split("/")[-2]
|
||||||
media_id = profile["avatar_url"].split("/")[-1]
|
media_id = profile["avatar_url"].split("/")[-1]
|
||||||
if self._is_mine_server_name(server_name):
|
if self._is_mine_server_name(server_name):
|
||||||
media = await self._media_repo.store.get_local_media(media_id)
|
media = await self._media_repo.store.get_local_media(media_id) # type: ignore[has-type]
|
||||||
if media is not None and upload_name == media.upload_name:
|
if media is not None and upload_name == media.upload_name:
|
||||||
logger.info("skipping saving the user avatar")
|
logger.info("skipping saving the user avatar")
|
||||||
return True
|
return True
|
||||||
|
@ -966,6 +971,7 @@ class SsoHandler:
|
||||||
session_id: str,
|
session_id: str,
|
||||||
localpart: str,
|
localpart: str,
|
||||||
use_display_name: bool,
|
use_display_name: bool,
|
||||||
|
use_avatar: bool,
|
||||||
emails_to_use: Iterable[str],
|
emails_to_use: Iterable[str],
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Handle a request to the username-picker 'submit' endpoint
|
"""Handle a request to the username-picker 'submit' endpoint
|
||||||
|
@ -988,6 +994,7 @@ class SsoHandler:
|
||||||
# update the session with the user's choices
|
# update the session with the user's choices
|
||||||
session.chosen_localpart = localpart
|
session.chosen_localpart = localpart
|
||||||
session.use_display_name = use_display_name
|
session.use_display_name = use_display_name
|
||||||
|
session.use_avatar = use_avatar
|
||||||
|
|
||||||
emails_from_idp = set(session.emails)
|
emails_from_idp = set(session.emails)
|
||||||
filtered_emails: Set[str] = set()
|
filtered_emails: Set[str] = set()
|
||||||
|
@ -1068,6 +1075,9 @@ class SsoHandler:
|
||||||
if session.use_display_name:
|
if session.use_display_name:
|
||||||
attributes.display_name = session.display_name
|
attributes.display_name = session.display_name
|
||||||
|
|
||||||
|
if session.use_avatar:
|
||||||
|
attributes.picture = session.avatar_url
|
||||||
|
|
||||||
# the following will raise a 400 error if the username has been taken in the
|
# the following will raise a 400 error if the username has been taken in the
|
||||||
# meantime.
|
# meantime.
|
||||||
user_id = await self._register_mapped_user(
|
user_id = await self._register_mapped_user(
|
||||||
|
|
|
@ -20,6 +20,7 @@
|
||||||
#
|
#
|
||||||
import itertools
|
import itertools
|
||||||
import logging
|
import logging
|
||||||
|
from enum import Enum
|
||||||
from typing import (
|
from typing import (
|
||||||
TYPE_CHECKING,
|
TYPE_CHECKING,
|
||||||
AbstractSet,
|
AbstractSet,
|
||||||
|
@ -27,11 +28,14 @@ from typing import (
|
||||||
Dict,
|
Dict,
|
||||||
FrozenSet,
|
FrozenSet,
|
||||||
List,
|
List,
|
||||||
|
Literal,
|
||||||
Mapping,
|
Mapping,
|
||||||
Optional,
|
Optional,
|
||||||
Sequence,
|
Sequence,
|
||||||
Set,
|
Set,
|
||||||
Tuple,
|
Tuple,
|
||||||
|
Union,
|
||||||
|
overload,
|
||||||
)
|
)
|
||||||
|
|
||||||
import attr
|
import attr
|
||||||
|
@ -112,12 +116,30 @@ LAZY_LOADED_MEMBERS_CACHE_MAX_SIZE = 100
|
||||||
SyncRequestKey = Tuple[Any, ...]
|
SyncRequestKey = Tuple[Any, ...]
|
||||||
|
|
||||||
|
|
||||||
|
class SyncVersion(Enum):
|
||||||
|
"""
|
||||||
|
Enum for specifying the version of sync request. This is used to key which type of
|
||||||
|
sync response that we are generating.
|
||||||
|
|
||||||
|
This is different than the `sync_type` you might see used in other code below; which
|
||||||
|
specifies the sub-type sync request (e.g. initial_sync, full_state_sync,
|
||||||
|
incremental_sync) and is really only relevant for the `/sync` v2 endpoint.
|
||||||
|
"""
|
||||||
|
|
||||||
|
# These string values are semantically significant because they are used in the the
|
||||||
|
# metrics
|
||||||
|
|
||||||
|
# Traditional `/sync` endpoint
|
||||||
|
SYNC_V2 = "sync_v2"
|
||||||
|
# Part of MSC3575 Sliding Sync
|
||||||
|
E2EE_SYNC = "e2ee_sync"
|
||||||
|
|
||||||
|
|
||||||
@attr.s(slots=True, frozen=True, auto_attribs=True)
|
@attr.s(slots=True, frozen=True, auto_attribs=True)
|
||||||
class SyncConfig:
|
class SyncConfig:
|
||||||
user: UserID
|
user: UserID
|
||||||
filter_collection: FilterCollection
|
filter_collection: FilterCollection
|
||||||
is_guest: bool
|
is_guest: bool
|
||||||
request_key: SyncRequestKey
|
|
||||||
device_id: Optional[str]
|
device_id: Optional[str]
|
||||||
|
|
||||||
|
|
||||||
|
@ -262,6 +284,43 @@ class SyncResult:
|
||||||
or self.device_lists
|
or self.device_lists
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def empty(next_batch: StreamToken) -> "SyncResult":
|
||||||
|
"Return a new empty result"
|
||||||
|
return SyncResult(
|
||||||
|
next_batch=next_batch,
|
||||||
|
presence=[],
|
||||||
|
account_data=[],
|
||||||
|
joined=[],
|
||||||
|
invited=[],
|
||||||
|
knocked=[],
|
||||||
|
archived=[],
|
||||||
|
to_device=[],
|
||||||
|
device_lists=DeviceListUpdates(),
|
||||||
|
device_one_time_keys_count={},
|
||||||
|
device_unused_fallback_key_types=[],
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@attr.s(slots=True, frozen=True, auto_attribs=True)
|
||||||
|
class E2eeSyncResult:
|
||||||
|
"""
|
||||||
|
Attributes:
|
||||||
|
next_batch: Token for the next sync
|
||||||
|
to_device: List of direct messages for the device.
|
||||||
|
device_lists: List of user_ids whose devices have changed
|
||||||
|
device_one_time_keys_count: Dict of algorithm to count for one time keys
|
||||||
|
for this device
|
||||||
|
device_unused_fallback_key_types: List of key types that have an unused fallback
|
||||||
|
key
|
||||||
|
"""
|
||||||
|
|
||||||
|
next_batch: StreamToken
|
||||||
|
to_device: List[JsonDict]
|
||||||
|
device_lists: DeviceListUpdates
|
||||||
|
device_one_time_keys_count: JsonMapping
|
||||||
|
device_unused_fallback_key_types: List[str]
|
||||||
|
|
||||||
|
|
||||||
class SyncHandler:
|
class SyncHandler:
|
||||||
def __init__(self, hs: "HomeServer"):
|
def __init__(self, hs: "HomeServer"):
|
||||||
|
@ -305,17 +364,68 @@ class SyncHandler:
|
||||||
|
|
||||||
self.rooms_to_exclude_globally = hs.config.server.rooms_to_exclude_from_sync
|
self.rooms_to_exclude_globally = hs.config.server.rooms_to_exclude_from_sync
|
||||||
|
|
||||||
|
@overload
|
||||||
async def wait_for_sync_for_user(
|
async def wait_for_sync_for_user(
|
||||||
self,
|
self,
|
||||||
requester: Requester,
|
requester: Requester,
|
||||||
sync_config: SyncConfig,
|
sync_config: SyncConfig,
|
||||||
|
sync_version: Literal[SyncVersion.SYNC_V2],
|
||||||
|
request_key: SyncRequestKey,
|
||||||
since_token: Optional[StreamToken] = None,
|
since_token: Optional[StreamToken] = None,
|
||||||
timeout: int = 0,
|
timeout: int = 0,
|
||||||
full_state: bool = False,
|
full_state: bool = False,
|
||||||
) -> SyncResult:
|
) -> SyncResult: ...
|
||||||
|
|
||||||
|
@overload
|
||||||
|
async def wait_for_sync_for_user(
|
||||||
|
self,
|
||||||
|
requester: Requester,
|
||||||
|
sync_config: SyncConfig,
|
||||||
|
sync_version: Literal[SyncVersion.E2EE_SYNC],
|
||||||
|
request_key: SyncRequestKey,
|
||||||
|
since_token: Optional[StreamToken] = None,
|
||||||
|
timeout: int = 0,
|
||||||
|
full_state: bool = False,
|
||||||
|
) -> E2eeSyncResult: ...
|
||||||
|
|
||||||
|
@overload
|
||||||
|
async def wait_for_sync_for_user(
|
||||||
|
self,
|
||||||
|
requester: Requester,
|
||||||
|
sync_config: SyncConfig,
|
||||||
|
sync_version: SyncVersion,
|
||||||
|
request_key: SyncRequestKey,
|
||||||
|
since_token: Optional[StreamToken] = None,
|
||||||
|
timeout: int = 0,
|
||||||
|
full_state: bool = False,
|
||||||
|
) -> Union[SyncResult, E2eeSyncResult]: ...
|
||||||
|
|
||||||
|
async def wait_for_sync_for_user(
|
||||||
|
self,
|
||||||
|
requester: Requester,
|
||||||
|
sync_config: SyncConfig,
|
||||||
|
sync_version: SyncVersion,
|
||||||
|
request_key: SyncRequestKey,
|
||||||
|
since_token: Optional[StreamToken] = None,
|
||||||
|
timeout: int = 0,
|
||||||
|
full_state: bool = False,
|
||||||
|
) -> Union[SyncResult, E2eeSyncResult]:
|
||||||
"""Get the sync for a client if we have new data for it now. Otherwise
|
"""Get the sync for a client if we have new data for it now. Otherwise
|
||||||
wait for new data to arrive on the server. If the timeout expires, then
|
wait for new data to arrive on the server. If the timeout expires, then
|
||||||
return an empty sync result.
|
return an empty sync result.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
requester: The user requesting the sync response.
|
||||||
|
sync_config: Config/info necessary to process the sync request.
|
||||||
|
sync_version: Determines what kind of sync response to generate.
|
||||||
|
request_key: The key to use for caching the response.
|
||||||
|
since_token: The point in the stream to sync from.
|
||||||
|
timeout: How long to wait for new data to arrive before giving up.
|
||||||
|
full_state: Whether to return the full state for each room.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
When `SyncVersion.SYNC_V2`, returns a full `SyncResult`.
|
||||||
|
When `SyncVersion.E2EE_SYNC`, returns a `E2eeSyncResult`.
|
||||||
"""
|
"""
|
||||||
# If the user is not part of the mau group, then check that limits have
|
# If the user is not part of the mau group, then check that limits have
|
||||||
# not been exceeded (if not part of the group by this point, almost certain
|
# not been exceeded (if not part of the group by this point, almost certain
|
||||||
|
@ -324,9 +434,10 @@ class SyncHandler:
|
||||||
await self.auth_blocking.check_auth_blocking(requester=requester)
|
await self.auth_blocking.check_auth_blocking(requester=requester)
|
||||||
|
|
||||||
res = await self.response_cache.wrap(
|
res = await self.response_cache.wrap(
|
||||||
sync_config.request_key,
|
request_key,
|
||||||
self._wait_for_sync_for_user,
|
self._wait_for_sync_for_user,
|
||||||
sync_config,
|
sync_config,
|
||||||
|
sync_version,
|
||||||
since_token,
|
since_token,
|
||||||
timeout,
|
timeout,
|
||||||
full_state,
|
full_state,
|
||||||
|
@ -335,14 +446,48 @@ class SyncHandler:
|
||||||
logger.debug("Returning sync response for %s", user_id)
|
logger.debug("Returning sync response for %s", user_id)
|
||||||
return res
|
return res
|
||||||
|
|
||||||
|
@overload
|
||||||
async def _wait_for_sync_for_user(
|
async def _wait_for_sync_for_user(
|
||||||
self,
|
self,
|
||||||
sync_config: SyncConfig,
|
sync_config: SyncConfig,
|
||||||
|
sync_version: Literal[SyncVersion.SYNC_V2],
|
||||||
since_token: Optional[StreamToken],
|
since_token: Optional[StreamToken],
|
||||||
timeout: int,
|
timeout: int,
|
||||||
full_state: bool,
|
full_state: bool,
|
||||||
cache_context: ResponseCacheContext[SyncRequestKey],
|
cache_context: ResponseCacheContext[SyncRequestKey],
|
||||||
) -> SyncResult:
|
) -> SyncResult: ...
|
||||||
|
|
||||||
|
@overload
|
||||||
|
async def _wait_for_sync_for_user(
|
||||||
|
self,
|
||||||
|
sync_config: SyncConfig,
|
||||||
|
sync_version: Literal[SyncVersion.E2EE_SYNC],
|
||||||
|
since_token: Optional[StreamToken],
|
||||||
|
timeout: int,
|
||||||
|
full_state: bool,
|
||||||
|
cache_context: ResponseCacheContext[SyncRequestKey],
|
||||||
|
) -> E2eeSyncResult: ...
|
||||||
|
|
||||||
|
@overload
|
||||||
|
async def _wait_for_sync_for_user(
|
||||||
|
self,
|
||||||
|
sync_config: SyncConfig,
|
||||||
|
sync_version: SyncVersion,
|
||||||
|
since_token: Optional[StreamToken],
|
||||||
|
timeout: int,
|
||||||
|
full_state: bool,
|
||||||
|
cache_context: ResponseCacheContext[SyncRequestKey],
|
||||||
|
) -> Union[SyncResult, E2eeSyncResult]: ...
|
||||||
|
|
||||||
|
async def _wait_for_sync_for_user(
|
||||||
|
self,
|
||||||
|
sync_config: SyncConfig,
|
||||||
|
sync_version: SyncVersion,
|
||||||
|
since_token: Optional[StreamToken],
|
||||||
|
timeout: int,
|
||||||
|
full_state: bool,
|
||||||
|
cache_context: ResponseCacheContext[SyncRequestKey],
|
||||||
|
) -> Union[SyncResult, E2eeSyncResult]:
|
||||||
"""The start of the machinery that produces a /sync response.
|
"""The start of the machinery that produces a /sync response.
|
||||||
|
|
||||||
See https://spec.matrix.org/v1.1/client-server-api/#syncing for full details.
|
See https://spec.matrix.org/v1.1/client-server-api/#syncing for full details.
|
||||||
|
@ -363,9 +508,29 @@ class SyncHandler:
|
||||||
else:
|
else:
|
||||||
sync_type = "incremental_sync"
|
sync_type = "incremental_sync"
|
||||||
|
|
||||||
|
sync_label = f"{sync_version}:{sync_type}"
|
||||||
|
|
||||||
context = current_context()
|
context = current_context()
|
||||||
if context:
|
if context:
|
||||||
context.tag = sync_type
|
context.tag = sync_label
|
||||||
|
|
||||||
|
if since_token is not None:
|
||||||
|
# We need to make sure this worker has caught up with the token. If
|
||||||
|
# this returns false it means we timed out waiting, and we should
|
||||||
|
# just return an empty response.
|
||||||
|
start = self.clock.time_msec()
|
||||||
|
if not await self.notifier.wait_for_stream_token(since_token):
|
||||||
|
logger.warning(
|
||||||
|
"Timed out waiting for worker to catch up. Returning empty response"
|
||||||
|
)
|
||||||
|
return SyncResult.empty(since_token)
|
||||||
|
|
||||||
|
# If we've spent significant time waiting to catch up, take it off
|
||||||
|
# the timeout.
|
||||||
|
now = self.clock.time_msec()
|
||||||
|
if now - start > 1_000:
|
||||||
|
timeout -= now - start
|
||||||
|
timeout = max(timeout, 0)
|
||||||
|
|
||||||
# if we have a since token, delete any to-device messages before that token
|
# if we have a since token, delete any to-device messages before that token
|
||||||
# (since we now know that the device has received them)
|
# (since we now know that the device has received them)
|
||||||
|
@ -383,15 +548,19 @@ class SyncHandler:
|
||||||
if timeout == 0 or since_token is None or full_state:
|
if timeout == 0 or since_token is None or full_state:
|
||||||
# we are going to return immediately, so don't bother calling
|
# we are going to return immediately, so don't bother calling
|
||||||
# notifier.wait_for_events.
|
# notifier.wait_for_events.
|
||||||
result: SyncResult = await self.current_sync_for_user(
|
result: Union[SyncResult, E2eeSyncResult] = (
|
||||||
sync_config, since_token, full_state=full_state
|
await self.current_sync_for_user(
|
||||||
|
sync_config, sync_version, since_token, full_state=full_state
|
||||||
|
)
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
# Otherwise, we wait for something to happen and report it to the user.
|
# Otherwise, we wait for something to happen and report it to the user.
|
||||||
async def current_sync_callback(
|
async def current_sync_callback(
|
||||||
before_token: StreamToken, after_token: StreamToken
|
before_token: StreamToken, after_token: StreamToken
|
||||||
) -> SyncResult:
|
) -> Union[SyncResult, E2eeSyncResult]:
|
||||||
return await self.current_sync_for_user(sync_config, since_token)
|
return await self.current_sync_for_user(
|
||||||
|
sync_config, sync_version, since_token
|
||||||
|
)
|
||||||
|
|
||||||
result = await self.notifier.wait_for_events(
|
result = await self.notifier.wait_for_events(
|
||||||
sync_config.user.to_string(),
|
sync_config.user.to_string(),
|
||||||
|
@ -416,27 +585,81 @@ class SyncHandler:
|
||||||
lazy_loaded = "true"
|
lazy_loaded = "true"
|
||||||
else:
|
else:
|
||||||
lazy_loaded = "false"
|
lazy_loaded = "false"
|
||||||
non_empty_sync_counter.labels(sync_type, lazy_loaded).inc()
|
non_empty_sync_counter.labels(sync_label, lazy_loaded).inc()
|
||||||
|
|
||||||
return result
|
return result
|
||||||
|
|
||||||
|
@overload
|
||||||
|
async def current_sync_for_user(
|
||||||
|
self,
|
||||||
|
sync_config: SyncConfig,
|
||||||
|
sync_version: Literal[SyncVersion.SYNC_V2],
|
||||||
|
since_token: Optional[StreamToken] = None,
|
||||||
|
full_state: bool = False,
|
||||||
|
) -> SyncResult: ...
|
||||||
|
|
||||||
|
@overload
|
||||||
|
async def current_sync_for_user(
|
||||||
|
self,
|
||||||
|
sync_config: SyncConfig,
|
||||||
|
sync_version: Literal[SyncVersion.E2EE_SYNC],
|
||||||
|
since_token: Optional[StreamToken] = None,
|
||||||
|
full_state: bool = False,
|
||||||
|
) -> E2eeSyncResult: ...
|
||||||
|
|
||||||
|
@overload
|
||||||
|
async def current_sync_for_user(
|
||||||
|
self,
|
||||||
|
sync_config: SyncConfig,
|
||||||
|
sync_version: SyncVersion,
|
||||||
|
since_token: Optional[StreamToken] = None,
|
||||||
|
full_state: bool = False,
|
||||||
|
) -> Union[SyncResult, E2eeSyncResult]: ...
|
||||||
|
|
||||||
async def current_sync_for_user(
|
async def current_sync_for_user(
|
||||||
self,
|
self,
|
||||||
sync_config: SyncConfig,
|
sync_config: SyncConfig,
|
||||||
|
sync_version: SyncVersion,
|
||||||
since_token: Optional[StreamToken] = None,
|
since_token: Optional[StreamToken] = None,
|
||||||
full_state: bool = False,
|
full_state: bool = False,
|
||||||
) -> SyncResult:
|
) -> Union[SyncResult, E2eeSyncResult]:
|
||||||
"""Generates the response body of a sync result, represented as a SyncResult.
|
"""
|
||||||
|
Generates the response body of a sync result, represented as a
|
||||||
|
`SyncResult`/`E2eeSyncResult`.
|
||||||
|
|
||||||
This is a wrapper around `generate_sync_result` which starts an open tracing
|
This is a wrapper around `generate_sync_result` which starts an open tracing
|
||||||
span to track the sync. See `generate_sync_result` for the next part of your
|
span to track the sync. See `generate_sync_result` for the next part of your
|
||||||
indoctrination.
|
indoctrination.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
sync_config: Config/info necessary to process the sync request.
|
||||||
|
sync_version: Determines what kind of sync response to generate.
|
||||||
|
since_token: The point in the stream to sync from.p.
|
||||||
|
full_state: Whether to return the full state for each room.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
When `SyncVersion.SYNC_V2`, returns a full `SyncResult`.
|
||||||
|
When `SyncVersion.E2EE_SYNC`, returns a `E2eeSyncResult`.
|
||||||
"""
|
"""
|
||||||
with start_active_span("sync.current_sync_for_user"):
|
with start_active_span("sync.current_sync_for_user"):
|
||||||
log_kv({"since_token": since_token})
|
log_kv({"since_token": since_token})
|
||||||
sync_result = await self.generate_sync_result(
|
|
||||||
sync_config, since_token, full_state
|
# Go through the `/sync` v2 path
|
||||||
)
|
if sync_version == SyncVersion.SYNC_V2:
|
||||||
|
sync_result: Union[SyncResult, E2eeSyncResult] = (
|
||||||
|
await self.generate_sync_result(
|
||||||
|
sync_config, since_token, full_state
|
||||||
|
)
|
||||||
|
)
|
||||||
|
# Go through the MSC3575 Sliding Sync `/sync/e2ee` path
|
||||||
|
elif sync_version == SyncVersion.E2EE_SYNC:
|
||||||
|
sync_result = await self.generate_e2ee_sync_result(
|
||||||
|
sync_config, since_token
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
raise Exception(
|
||||||
|
f"Unknown sync_version (this is a Synapse problem): {sync_version}"
|
||||||
|
)
|
||||||
|
|
||||||
set_tag(SynapseTags.SYNC_RESULT, bool(sync_result))
|
set_tag(SynapseTags.SYNC_RESULT, bool(sync_result))
|
||||||
return sync_result
|
return sync_result
|
||||||
|
@ -596,6 +819,7 @@ class SyncHandler:
|
||||||
sync_config.user.to_string(),
|
sync_config.user.to_string(),
|
||||||
recents,
|
recents,
|
||||||
always_include_ids=current_state_ids,
|
always_include_ids=current_state_ids,
|
||||||
|
msc4115_membership_on_events=self.hs_config.experimental.msc4115_membership_on_events,
|
||||||
)
|
)
|
||||||
log_kv({"recents_after_visibility_filtering": len(recents)})
|
log_kv({"recents_after_visibility_filtering": len(recents)})
|
||||||
else:
|
else:
|
||||||
|
@ -681,6 +905,7 @@ class SyncHandler:
|
||||||
sync_config.user.to_string(),
|
sync_config.user.to_string(),
|
||||||
loaded_recents,
|
loaded_recents,
|
||||||
always_include_ids=current_state_ids,
|
always_include_ids=current_state_ids,
|
||||||
|
msc4115_membership_on_events=self.hs_config.experimental.msc4115_membership_on_events,
|
||||||
)
|
)
|
||||||
|
|
||||||
loaded_recents = []
|
loaded_recents = []
|
||||||
|
@ -1124,7 +1349,6 @@ class SyncHandler:
|
||||||
for e in await sync_config.filter_collection.filter_room_state(
|
for e in await sync_config.filter_collection.filter_room_state(
|
||||||
list(state.values())
|
list(state.values())
|
||||||
)
|
)
|
||||||
if e.type != EventTypes.Aliases # until MSC2261 or alternative solution
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async def _compute_state_delta_for_full_sync(
|
async def _compute_state_delta_for_full_sync(
|
||||||
|
@ -1516,128 +1740,17 @@ class SyncHandler:
|
||||||
# See https://github.com/matrix-org/matrix-doc/issues/1144
|
# See https://github.com/matrix-org/matrix-doc/issues/1144
|
||||||
raise NotImplementedError()
|
raise NotImplementedError()
|
||||||
|
|
||||||
# Note: we get the users room list *before* we get the current token, this
|
sync_result_builder = await self.get_sync_result_builder(
|
||||||
# avoids checking back in history if rooms are joined after the token is fetched.
|
sync_config,
|
||||||
token_before_rooms = self.event_sources.get_current_token()
|
since_token,
|
||||||
mutable_joined_room_ids = set(await self.store.get_rooms_for_user(user_id))
|
full_state,
|
||||||
|
|
||||||
# NB: The now_token gets changed by some of the generate_sync_* methods,
|
|
||||||
# this is due to some of the underlying streams not supporting the ability
|
|
||||||
# to query up to a given point.
|
|
||||||
# Always use the `now_token` in `SyncResultBuilder`
|
|
||||||
now_token = self.event_sources.get_current_token()
|
|
||||||
log_kv({"now_token": now_token})
|
|
||||||
|
|
||||||
# Since we fetched the users room list before the token, there's a small window
|
|
||||||
# during which membership events may have been persisted, so we fetch these now
|
|
||||||
# and modify the joined room list for any changes between the get_rooms_for_user
|
|
||||||
# call and the get_current_token call.
|
|
||||||
membership_change_events = []
|
|
||||||
if since_token:
|
|
||||||
membership_change_events = await self.store.get_membership_changes_for_user(
|
|
||||||
user_id,
|
|
||||||
since_token.room_key,
|
|
||||||
now_token.room_key,
|
|
||||||
self.rooms_to_exclude_globally,
|
|
||||||
)
|
|
||||||
|
|
||||||
mem_last_change_by_room_id: Dict[str, EventBase] = {}
|
|
||||||
for event in membership_change_events:
|
|
||||||
mem_last_change_by_room_id[event.room_id] = event
|
|
||||||
|
|
||||||
# For the latest membership event in each room found, add/remove the room ID
|
|
||||||
# from the joined room list accordingly. In this case we only care if the
|
|
||||||
# latest change is JOIN.
|
|
||||||
|
|
||||||
for room_id, event in mem_last_change_by_room_id.items():
|
|
||||||
assert event.internal_metadata.stream_ordering
|
|
||||||
if (
|
|
||||||
event.internal_metadata.stream_ordering
|
|
||||||
< token_before_rooms.room_key.stream
|
|
||||||
):
|
|
||||||
continue
|
|
||||||
|
|
||||||
logger.info(
|
|
||||||
"User membership change between getting rooms and current token: %s %s %s",
|
|
||||||
user_id,
|
|
||||||
event.membership,
|
|
||||||
room_id,
|
|
||||||
)
|
|
||||||
# User joined a room - we have to then check the room state to ensure we
|
|
||||||
# respect any bans if there's a race between the join and ban events.
|
|
||||||
if event.membership == Membership.JOIN:
|
|
||||||
user_ids_in_room = await self.store.get_users_in_room(room_id)
|
|
||||||
if user_id in user_ids_in_room:
|
|
||||||
mutable_joined_room_ids.add(room_id)
|
|
||||||
# The user left the room, or left and was re-invited but not joined yet
|
|
||||||
else:
|
|
||||||
mutable_joined_room_ids.discard(room_id)
|
|
||||||
|
|
||||||
# Tweak the set of rooms to return to the client for eager (non-lazy) syncs.
|
|
||||||
mutable_rooms_to_exclude = set(self.rooms_to_exclude_globally)
|
|
||||||
if not sync_config.filter_collection.lazy_load_members():
|
|
||||||
# Non-lazy syncs should never include partially stated rooms.
|
|
||||||
# Exclude all partially stated rooms from this sync.
|
|
||||||
results = await self.store.is_partial_state_room_batched(
|
|
||||||
mutable_joined_room_ids
|
|
||||||
)
|
|
||||||
mutable_rooms_to_exclude.update(
|
|
||||||
room_id
|
|
||||||
for room_id, is_partial_state in results.items()
|
|
||||||
if is_partial_state
|
|
||||||
)
|
|
||||||
membership_change_events = [
|
|
||||||
event
|
|
||||||
for event in membership_change_events
|
|
||||||
if not results.get(event.room_id, False)
|
|
||||||
]
|
|
||||||
|
|
||||||
# Incremental eager syncs should additionally include rooms that
|
|
||||||
# - we are joined to
|
|
||||||
# - are full-stated
|
|
||||||
# - became fully-stated at some point during the sync period
|
|
||||||
# (These rooms will have been omitted during a previous eager sync.)
|
|
||||||
forced_newly_joined_room_ids: Set[str] = set()
|
|
||||||
if since_token and not sync_config.filter_collection.lazy_load_members():
|
|
||||||
un_partial_stated_rooms = (
|
|
||||||
await self.store.get_un_partial_stated_rooms_between(
|
|
||||||
since_token.un_partial_stated_rooms_key,
|
|
||||||
now_token.un_partial_stated_rooms_key,
|
|
||||||
mutable_joined_room_ids,
|
|
||||||
)
|
|
||||||
)
|
|
||||||
results = await self.store.is_partial_state_room_batched(
|
|
||||||
un_partial_stated_rooms
|
|
||||||
)
|
|
||||||
forced_newly_joined_room_ids.update(
|
|
||||||
room_id
|
|
||||||
for room_id, is_partial_state in results.items()
|
|
||||||
if not is_partial_state
|
|
||||||
)
|
|
||||||
|
|
||||||
# Now we have our list of joined room IDs, exclude as configured and freeze
|
|
||||||
joined_room_ids = frozenset(
|
|
||||||
room_id
|
|
||||||
for room_id in mutable_joined_room_ids
|
|
||||||
if room_id not in mutable_rooms_to_exclude
|
|
||||||
)
|
)
|
||||||
|
|
||||||
logger.debug(
|
logger.debug(
|
||||||
"Calculating sync response for %r between %s and %s",
|
"Calculating sync response for %r between %s and %s",
|
||||||
sync_config.user,
|
sync_config.user,
|
||||||
since_token,
|
sync_result_builder.since_token,
|
||||||
now_token,
|
sync_result_builder.now_token,
|
||||||
)
|
|
||||||
|
|
||||||
sync_result_builder = SyncResultBuilder(
|
|
||||||
sync_config,
|
|
||||||
full_state,
|
|
||||||
since_token=since_token,
|
|
||||||
now_token=now_token,
|
|
||||||
joined_room_ids=joined_room_ids,
|
|
||||||
excluded_room_ids=frozenset(mutable_rooms_to_exclude),
|
|
||||||
forced_newly_joined_room_ids=frozenset(forced_newly_joined_room_ids),
|
|
||||||
membership_change_events=membership_change_events,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
logger.debug("Fetching account data")
|
logger.debug("Fetching account data")
|
||||||
|
@ -1749,6 +1862,239 @@ class SyncHandler:
|
||||||
next_batch=sync_result_builder.now_token,
|
next_batch=sync_result_builder.now_token,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
async def generate_e2ee_sync_result(
|
||||||
|
self,
|
||||||
|
sync_config: SyncConfig,
|
||||||
|
since_token: Optional[StreamToken] = None,
|
||||||
|
) -> E2eeSyncResult:
|
||||||
|
"""
|
||||||
|
Generates the response body of a MSC3575 Sliding Sync `/sync/e2ee` result.
|
||||||
|
|
||||||
|
This is represented by a `E2eeSyncResult` struct, which is built from small
|
||||||
|
pieces using a `SyncResultBuilder`. The `sync_result_builder` is passed as a
|
||||||
|
mutable ("inout") parameter to various helper functions. These retrieve and
|
||||||
|
process the data which forms the sync body, often writing to the
|
||||||
|
`sync_result_builder` to store their output.
|
||||||
|
|
||||||
|
At the end, we transfer data from the `sync_result_builder` to a new `E2eeSyncResult`
|
||||||
|
instance to signify that the sync calculation is complete.
|
||||||
|
"""
|
||||||
|
user_id = sync_config.user.to_string()
|
||||||
|
app_service = self.store.get_app_service_by_user_id(user_id)
|
||||||
|
if app_service:
|
||||||
|
# We no longer support AS users using /sync directly.
|
||||||
|
# See https://github.com/matrix-org/matrix-doc/issues/1144
|
||||||
|
raise NotImplementedError()
|
||||||
|
|
||||||
|
sync_result_builder = await self.get_sync_result_builder(
|
||||||
|
sync_config,
|
||||||
|
since_token,
|
||||||
|
full_state=False,
|
||||||
|
)
|
||||||
|
|
||||||
|
# 1. Calculate `to_device` events
|
||||||
|
await self._generate_sync_entry_for_to_device(sync_result_builder)
|
||||||
|
|
||||||
|
# 2. Calculate `device_lists`
|
||||||
|
# Device list updates are sent if a since token is provided.
|
||||||
|
device_lists = DeviceListUpdates()
|
||||||
|
include_device_list_updates = bool(since_token and since_token.device_list_key)
|
||||||
|
if include_device_list_updates:
|
||||||
|
# Note that _generate_sync_entry_for_rooms sets sync_result_builder.joined, which
|
||||||
|
# is used in calculate_user_changes below.
|
||||||
|
#
|
||||||
|
# TODO: Running `_generate_sync_entry_for_rooms()` is a lot of work just to
|
||||||
|
# figure out the membership changes/derived info needed for
|
||||||
|
# `_generate_sync_entry_for_device_list()`. In the future, we should try to
|
||||||
|
# refactor this away.
|
||||||
|
(
|
||||||
|
newly_joined_rooms,
|
||||||
|
newly_left_rooms,
|
||||||
|
) = await self._generate_sync_entry_for_rooms(sync_result_builder)
|
||||||
|
|
||||||
|
# This uses the sync_result_builder.joined which is set in
|
||||||
|
# `_generate_sync_entry_for_rooms`, if that didn't find any joined
|
||||||
|
# rooms for some reason it is a no-op.
|
||||||
|
(
|
||||||
|
newly_joined_or_invited_or_knocked_users,
|
||||||
|
newly_left_users,
|
||||||
|
) = sync_result_builder.calculate_user_changes()
|
||||||
|
|
||||||
|
device_lists = await self._generate_sync_entry_for_device_list(
|
||||||
|
sync_result_builder,
|
||||||
|
newly_joined_rooms=newly_joined_rooms,
|
||||||
|
newly_joined_or_invited_or_knocked_users=newly_joined_or_invited_or_knocked_users,
|
||||||
|
newly_left_rooms=newly_left_rooms,
|
||||||
|
newly_left_users=newly_left_users,
|
||||||
|
)
|
||||||
|
|
||||||
|
# 3. Calculate `device_one_time_keys_count` and `device_unused_fallback_key_types`
|
||||||
|
device_id = sync_config.device_id
|
||||||
|
one_time_keys_count: JsonMapping = {}
|
||||||
|
unused_fallback_key_types: List[str] = []
|
||||||
|
if device_id:
|
||||||
|
# TODO: We should have a way to let clients differentiate between the states of:
|
||||||
|
# * no change in OTK count since the provided since token
|
||||||
|
# * the server has zero OTKs left for this device
|
||||||
|
# Spec issue: https://github.com/matrix-org/matrix-doc/issues/3298
|
||||||
|
one_time_keys_count = await self.store.count_e2e_one_time_keys(
|
||||||
|
user_id, device_id
|
||||||
|
)
|
||||||
|
unused_fallback_key_types = list(
|
||||||
|
await self.store.get_e2e_unused_fallback_key_types(user_id, device_id)
|
||||||
|
)
|
||||||
|
|
||||||
|
return E2eeSyncResult(
|
||||||
|
to_device=sync_result_builder.to_device,
|
||||||
|
device_lists=device_lists,
|
||||||
|
device_one_time_keys_count=one_time_keys_count,
|
||||||
|
device_unused_fallback_key_types=unused_fallback_key_types,
|
||||||
|
next_batch=sync_result_builder.now_token,
|
||||||
|
)
|
||||||
|
|
||||||
|
async def get_sync_result_builder(
|
||||||
|
self,
|
||||||
|
sync_config: SyncConfig,
|
||||||
|
since_token: Optional[StreamToken] = None,
|
||||||
|
full_state: bool = False,
|
||||||
|
) -> "SyncResultBuilder":
|
||||||
|
"""
|
||||||
|
Assemble a `SyncResultBuilder` with all of the initial context to
|
||||||
|
start building up the sync response:
|
||||||
|
|
||||||
|
- Membership changes between the last sync and the current sync.
|
||||||
|
- Joined room IDs (minus any rooms to exclude).
|
||||||
|
- Rooms that became fully-stated/un-partial stated since the last sync.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
sync_config: Config/info necessary to process the sync request.
|
||||||
|
since_token: The point in the stream to sync from.
|
||||||
|
full_state: Whether to return the full state for each room.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
`SyncResultBuilder` ready to start generating parts of the sync response.
|
||||||
|
"""
|
||||||
|
user_id = sync_config.user.to_string()
|
||||||
|
|
||||||
|
# Note: we get the users room list *before* we get the current token, this
|
||||||
|
# avoids checking back in history if rooms are joined after the token is fetched.
|
||||||
|
token_before_rooms = self.event_sources.get_current_token()
|
||||||
|
mutable_joined_room_ids = set(await self.store.get_rooms_for_user(user_id))
|
||||||
|
|
||||||
|
# NB: The `now_token` gets changed by some of the `generate_sync_*` methods,
|
||||||
|
# this is due to some of the underlying streams not supporting the ability
|
||||||
|
# to query up to a given point.
|
||||||
|
# Always use the `now_token` in `SyncResultBuilder`
|
||||||
|
now_token = self.event_sources.get_current_token()
|
||||||
|
log_kv({"now_token": now_token})
|
||||||
|
|
||||||
|
# Since we fetched the users room list before the token, there's a small window
|
||||||
|
# during which membership events may have been persisted, so we fetch these now
|
||||||
|
# and modify the joined room list for any changes between the get_rooms_for_user
|
||||||
|
# call and the get_current_token call.
|
||||||
|
membership_change_events = []
|
||||||
|
if since_token:
|
||||||
|
membership_change_events = await self.store.get_membership_changes_for_user(
|
||||||
|
user_id,
|
||||||
|
since_token.room_key,
|
||||||
|
now_token.room_key,
|
||||||
|
self.rooms_to_exclude_globally,
|
||||||
|
)
|
||||||
|
|
||||||
|
mem_last_change_by_room_id: Dict[str, EventBase] = {}
|
||||||
|
for event in membership_change_events:
|
||||||
|
mem_last_change_by_room_id[event.room_id] = event
|
||||||
|
|
||||||
|
# For the latest membership event in each room found, add/remove the room ID
|
||||||
|
# from the joined room list accordingly. In this case we only care if the
|
||||||
|
# latest change is JOIN.
|
||||||
|
|
||||||
|
for room_id, event in mem_last_change_by_room_id.items():
|
||||||
|
assert event.internal_metadata.stream_ordering
|
||||||
|
if (
|
||||||
|
event.internal_metadata.stream_ordering
|
||||||
|
< token_before_rooms.room_key.stream
|
||||||
|
):
|
||||||
|
continue
|
||||||
|
|
||||||
|
logger.info(
|
||||||
|
"User membership change between getting rooms and current token: %s %s %s",
|
||||||
|
user_id,
|
||||||
|
event.membership,
|
||||||
|
room_id,
|
||||||
|
)
|
||||||
|
# User joined a room - we have to then check the room state to ensure we
|
||||||
|
# respect any bans if there's a race between the join and ban events.
|
||||||
|
if event.membership == Membership.JOIN:
|
||||||
|
user_ids_in_room = await self.store.get_users_in_room(room_id)
|
||||||
|
if user_id in user_ids_in_room:
|
||||||
|
mutable_joined_room_ids.add(room_id)
|
||||||
|
# The user left the room, or left and was re-invited but not joined yet
|
||||||
|
else:
|
||||||
|
mutable_joined_room_ids.discard(room_id)
|
||||||
|
|
||||||
|
# Tweak the set of rooms to return to the client for eager (non-lazy) syncs.
|
||||||
|
mutable_rooms_to_exclude = set(self.rooms_to_exclude_globally)
|
||||||
|
if not sync_config.filter_collection.lazy_load_members():
|
||||||
|
# Non-lazy syncs should never include partially stated rooms.
|
||||||
|
# Exclude all partially stated rooms from this sync.
|
||||||
|
results = await self.store.is_partial_state_room_batched(
|
||||||
|
mutable_joined_room_ids
|
||||||
|
)
|
||||||
|
mutable_rooms_to_exclude.update(
|
||||||
|
room_id
|
||||||
|
for room_id, is_partial_state in results.items()
|
||||||
|
if is_partial_state
|
||||||
|
)
|
||||||
|
membership_change_events = [
|
||||||
|
event
|
||||||
|
for event in membership_change_events
|
||||||
|
if not results.get(event.room_id, False)
|
||||||
|
]
|
||||||
|
|
||||||
|
# Incremental eager syncs should additionally include rooms that
|
||||||
|
# - we are joined to
|
||||||
|
# - are full-stated
|
||||||
|
# - became fully-stated at some point during the sync period
|
||||||
|
# (These rooms will have been omitted during a previous eager sync.)
|
||||||
|
forced_newly_joined_room_ids: Set[str] = set()
|
||||||
|
if since_token and not sync_config.filter_collection.lazy_load_members():
|
||||||
|
un_partial_stated_rooms = (
|
||||||
|
await self.store.get_un_partial_stated_rooms_between(
|
||||||
|
since_token.un_partial_stated_rooms_key,
|
||||||
|
now_token.un_partial_stated_rooms_key,
|
||||||
|
mutable_joined_room_ids,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
results = await self.store.is_partial_state_room_batched(
|
||||||
|
un_partial_stated_rooms
|
||||||
|
)
|
||||||
|
forced_newly_joined_room_ids.update(
|
||||||
|
room_id
|
||||||
|
for room_id, is_partial_state in results.items()
|
||||||
|
if not is_partial_state
|
||||||
|
)
|
||||||
|
|
||||||
|
# Now we have our list of joined room IDs, exclude as configured and freeze
|
||||||
|
joined_room_ids = frozenset(
|
||||||
|
room_id
|
||||||
|
for room_id in mutable_joined_room_ids
|
||||||
|
if room_id not in mutable_rooms_to_exclude
|
||||||
|
)
|
||||||
|
|
||||||
|
sync_result_builder = SyncResultBuilder(
|
||||||
|
sync_config,
|
||||||
|
full_state,
|
||||||
|
since_token=since_token,
|
||||||
|
now_token=now_token,
|
||||||
|
joined_room_ids=joined_room_ids,
|
||||||
|
excluded_room_ids=frozenset(mutable_rooms_to_exclude),
|
||||||
|
forced_newly_joined_room_ids=frozenset(forced_newly_joined_room_ids),
|
||||||
|
membership_change_events=membership_change_events,
|
||||||
|
)
|
||||||
|
|
||||||
|
return sync_result_builder
|
||||||
|
|
||||||
@measure_func("_generate_sync_entry_for_device_list")
|
@measure_func("_generate_sync_entry_for_device_list")
|
||||||
async def _generate_sync_entry_for_device_list(
|
async def _generate_sync_entry_for_device_list(
|
||||||
self,
|
self,
|
||||||
|
@ -1797,42 +2143,18 @@ class SyncHandler:
|
||||||
|
|
||||||
users_that_have_changed = set()
|
users_that_have_changed = set()
|
||||||
|
|
||||||
joined_rooms = sync_result_builder.joined_room_ids
|
joined_room_ids = sync_result_builder.joined_room_ids
|
||||||
|
|
||||||
# Step 1a, check for changes in devices of users we share a room
|
# Step 1a, check for changes in devices of users we share a room
|
||||||
# with
|
# with
|
||||||
#
|
users_that_have_changed = (
|
||||||
# We do this in two different ways depending on what we have cached.
|
await self._device_handler.get_device_changes_in_shared_rooms(
|
||||||
# If we already have a list of all the user that have changed since
|
user_id,
|
||||||
# the last sync then it's likely more efficient to compare the rooms
|
joined_room_ids,
|
||||||
# they're in with the rooms the syncing user is in.
|
from_token=since_token,
|
||||||
#
|
now_token=sync_result_builder.now_token,
|
||||||
# If we don't have that info cached then we get all the users that
|
|
||||||
# share a room with our user and check if those users have changed.
|
|
||||||
cache_result = self.store.get_cached_device_list_changes(
|
|
||||||
since_token.device_list_key
|
|
||||||
)
|
|
||||||
if cache_result.hit:
|
|
||||||
changed_users = cache_result.entities
|
|
||||||
|
|
||||||
result = await self.store.get_rooms_for_users(changed_users)
|
|
||||||
|
|
||||||
for changed_user_id, entries in result.items():
|
|
||||||
# Check if the changed user shares any rooms with the user,
|
|
||||||
# or if the changed user is the syncing user (as we always
|
|
||||||
# want to include device list updates of their own devices).
|
|
||||||
if user_id == changed_user_id or any(
|
|
||||||
rid in joined_rooms for rid in entries
|
|
||||||
):
|
|
||||||
users_that_have_changed.add(changed_user_id)
|
|
||||||
else:
|
|
||||||
users_that_have_changed = (
|
|
||||||
await self._device_handler.get_device_changes_in_shared_rooms(
|
|
||||||
user_id,
|
|
||||||
sync_result_builder.joined_room_ids,
|
|
||||||
from_token=since_token,
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
|
)
|
||||||
|
|
||||||
# Step 1b, check for newly joined rooms
|
# Step 1b, check for newly joined rooms
|
||||||
for room_id in newly_joined_rooms:
|
for room_id in newly_joined_rooms:
|
||||||
|
@ -1856,7 +2178,7 @@ class SyncHandler:
|
||||||
# Remove any users that we still share a room with.
|
# Remove any users that we still share a room with.
|
||||||
left_users_rooms = await self.store.get_rooms_for_users(newly_left_users)
|
left_users_rooms = await self.store.get_rooms_for_users(newly_left_users)
|
||||||
for user_id, entries in left_users_rooms.items():
|
for user_id, entries in left_users_rooms.items():
|
||||||
if any(rid in joined_rooms for rid in entries):
|
if any(rid in joined_room_ids for rid in entries):
|
||||||
newly_left_users.discard(user_id)
|
newly_left_users.discard(user_id)
|
||||||
|
|
||||||
return DeviceListUpdates(changed=users_that_have_changed, left=newly_left_users)
|
return DeviceListUpdates(changed=users_that_have_changed, left=newly_left_users)
|
||||||
|
@ -1943,23 +2265,19 @@ class SyncHandler:
|
||||||
)
|
)
|
||||||
|
|
||||||
if push_rules_changed:
|
if push_rules_changed:
|
||||||
global_account_data = {
|
global_account_data = dict(global_account_data)
|
||||||
AccountDataTypes.PUSH_RULES: await self._push_rules_handler.push_rules_for_user(
|
global_account_data[AccountDataTypes.PUSH_RULES] = (
|
||||||
sync_config.user
|
await self._push_rules_handler.push_rules_for_user(sync_config.user)
|
||||||
),
|
)
|
||||||
**global_account_data,
|
|
||||||
}
|
|
||||||
else:
|
else:
|
||||||
all_global_account_data = await self.store.get_global_account_data_for_user(
|
all_global_account_data = await self.store.get_global_account_data_for_user(
|
||||||
user_id
|
user_id
|
||||||
)
|
)
|
||||||
|
|
||||||
global_account_data = {
|
global_account_data = dict(all_global_account_data)
|
||||||
AccountDataTypes.PUSH_RULES: await self._push_rules_handler.push_rules_for_user(
|
global_account_data[AccountDataTypes.PUSH_RULES] = (
|
||||||
sync_config.user
|
await self._push_rules_handler.push_rules_for_user(sync_config.user)
|
||||||
),
|
)
|
||||||
**all_global_account_data,
|
|
||||||
}
|
|
||||||
|
|
||||||
account_data_for_user = (
|
account_data_for_user = (
|
||||||
await sync_config.filter_collection.filter_global_account_data(
|
await sync_config.filter_collection.filter_global_account_data(
|
||||||
|
|
|
@ -477,9 +477,9 @@ class TypingWriterHandler(FollowerTypingHandler):
|
||||||
|
|
||||||
rows = []
|
rows = []
|
||||||
for room_id in changed_rooms:
|
for room_id in changed_rooms:
|
||||||
serial = self._room_serials[room_id]
|
serial = self._room_serials.get(room_id)
|
||||||
if last_id < serial <= current_id:
|
if serial and last_id < serial <= current_id:
|
||||||
typing = self._room_typing[room_id]
|
typing = self._room_typing.get(room_id, set())
|
||||||
rows.append((serial, [room_id, list(typing)]))
|
rows.append((serial, [room_id, list(typing)]))
|
||||||
rows.sort()
|
rows.sort()
|
||||||
|
|
||||||
|
|
|
@ -262,7 +262,8 @@ class _ProxyResponseBody(protocol.Protocol):
|
||||||
self._request.finish()
|
self._request.finish()
|
||||||
else:
|
else:
|
||||||
# Abort the underlying request since our remote request also failed.
|
# Abort the underlying request since our remote request also failed.
|
||||||
self._request.transport.abortConnection()
|
if self._request.channel:
|
||||||
|
self._request.channel.forceAbortClient()
|
||||||
|
|
||||||
|
|
||||||
class ProxySite(Site):
|
class ProxySite(Site):
|
||||||
|
|
|
@ -153,9 +153,9 @@ def return_json_error(
|
||||||
# Only respond with an error response if we haven't already started writing,
|
# Only respond with an error response if we haven't already started writing,
|
||||||
# otherwise lets just kill the connection
|
# otherwise lets just kill the connection
|
||||||
if request.startedWriting:
|
if request.startedWriting:
|
||||||
if request.transport:
|
if request.channel:
|
||||||
try:
|
try:
|
||||||
request.transport.abortConnection()
|
request.channel.forceAbortClient()
|
||||||
except Exception:
|
except Exception:
|
||||||
# abortConnection throws if the connection is already closed
|
# abortConnection throws if the connection is already closed
|
||||||
pass
|
pass
|
||||||
|
@ -909,7 +909,19 @@ def set_cors_headers(request: "SynapseRequest") -> None:
|
||||||
request.setHeader(
|
request.setHeader(
|
||||||
b"Access-Control-Allow-Methods", b"GET, HEAD, POST, PUT, DELETE, OPTIONS"
|
b"Access-Control-Allow-Methods", b"GET, HEAD, POST, PUT, DELETE, OPTIONS"
|
||||||
)
|
)
|
||||||
if request.experimental_cors_msc3886:
|
if request.path is not None and (
|
||||||
|
request.path == b"/_matrix/client/unstable/org.matrix.msc4108/rendezvous"
|
||||||
|
or request.path.startswith(b"/_synapse/client/rendezvous")
|
||||||
|
):
|
||||||
|
request.setHeader(
|
||||||
|
b"Access-Control-Allow-Headers",
|
||||||
|
b"Content-Type, If-Match, If-None-Match",
|
||||||
|
)
|
||||||
|
request.setHeader(
|
||||||
|
b"Access-Control-Expose-Headers",
|
||||||
|
b"Synapse-Trace-Id, Server, ETag",
|
||||||
|
)
|
||||||
|
elif request.experimental_cors_msc3886:
|
||||||
request.setHeader(
|
request.setHeader(
|
||||||
b"Access-Control-Allow-Headers",
|
b"Access-Control-Allow-Headers",
|
||||||
b"X-Requested-With, Content-Type, Authorization, Date, If-Match, If-None-Match",
|
b"X-Requested-With, Content-Type, Authorization, Date, If-Match, If-None-Match",
|
||||||
|
|
|
@ -19,9 +19,11 @@
|
||||||
#
|
#
|
||||||
#
|
#
|
||||||
|
|
||||||
""" This module contains base REST classes for constructing REST servlets. """
|
"""This module contains base REST classes for constructing REST servlets."""
|
||||||
|
|
||||||
import enum
|
import enum
|
||||||
import logging
|
import logging
|
||||||
|
import urllib.parse as urlparse
|
||||||
from http import HTTPStatus
|
from http import HTTPStatus
|
||||||
from typing import (
|
from typing import (
|
||||||
TYPE_CHECKING,
|
TYPE_CHECKING,
|
||||||
|
@ -65,17 +67,49 @@ def parse_integer(request: Request, name: str, default: int) -> int: ...
|
||||||
|
|
||||||
|
|
||||||
@overload
|
@overload
|
||||||
def parse_integer(request: Request, name: str, *, required: Literal[True]) -> int: ...
|
def parse_integer(
|
||||||
|
request: Request, name: str, *, default: int, negative: bool
|
||||||
|
) -> int: ...
|
||||||
|
|
||||||
|
|
||||||
@overload
|
@overload
|
||||||
def parse_integer(
|
def parse_integer(
|
||||||
request: Request, name: str, default: Optional[int] = None, required: bool = False
|
request: Request, name: str, *, default: int, negative: bool = False
|
||||||
|
) -> int: ...
|
||||||
|
|
||||||
|
|
||||||
|
@overload
|
||||||
|
def parse_integer(
|
||||||
|
request: Request, name: str, *, required: Literal[True], negative: bool = False
|
||||||
|
) -> int: ...
|
||||||
|
|
||||||
|
|
||||||
|
@overload
|
||||||
|
def parse_integer(
|
||||||
|
request: Request, name: str, *, default: Literal[None], negative: bool = False
|
||||||
|
) -> None: ...
|
||||||
|
|
||||||
|
|
||||||
|
@overload
|
||||||
|
def parse_integer(request: Request, name: str, *, negative: bool) -> Optional[int]: ...
|
||||||
|
|
||||||
|
|
||||||
|
@overload
|
||||||
|
def parse_integer(
|
||||||
|
request: Request,
|
||||||
|
name: str,
|
||||||
|
default: Optional[int] = None,
|
||||||
|
required: bool = False,
|
||||||
|
negative: bool = False,
|
||||||
) -> Optional[int]: ...
|
) -> Optional[int]: ...
|
||||||
|
|
||||||
|
|
||||||
def parse_integer(
|
def parse_integer(
|
||||||
request: Request, name: str, default: Optional[int] = None, required: bool = False
|
request: Request,
|
||||||
|
name: str,
|
||||||
|
default: Optional[int] = None,
|
||||||
|
required: bool = False,
|
||||||
|
negative: bool = False,
|
||||||
) -> Optional[int]:
|
) -> Optional[int]:
|
||||||
"""Parse an integer parameter from the request string
|
"""Parse an integer parameter from the request string
|
||||||
|
|
||||||
|
@ -85,16 +119,17 @@ def parse_integer(
|
||||||
default: value to use if the parameter is absent, defaults to None.
|
default: value to use if the parameter is absent, defaults to None.
|
||||||
required: whether to raise a 400 SynapseError if the parameter is absent,
|
required: whether to raise a 400 SynapseError if the parameter is absent,
|
||||||
defaults to False.
|
defaults to False.
|
||||||
|
negative: whether to allow negative integers, defaults to True.
|
||||||
Returns:
|
Returns:
|
||||||
An int value or the default.
|
An int value or the default.
|
||||||
|
|
||||||
Raises:
|
Raises:
|
||||||
SynapseError: if the parameter is absent and required, or if the
|
SynapseError: if the parameter is absent and required, if the
|
||||||
parameter is present and not an integer.
|
parameter is present and not an integer, or if the
|
||||||
|
parameter is illegitimate negative.
|
||||||
"""
|
"""
|
||||||
args: Mapping[bytes, Sequence[bytes]] = request.args # type: ignore
|
args: Mapping[bytes, Sequence[bytes]] = request.args # type: ignore
|
||||||
return parse_integer_from_args(args, name, default, required)
|
return parse_integer_from_args(args, name, default, required, negative)
|
||||||
|
|
||||||
|
|
||||||
@overload
|
@overload
|
||||||
|
@ -120,6 +155,7 @@ def parse_integer_from_args(
|
||||||
name: str,
|
name: str,
|
||||||
default: Optional[int] = None,
|
default: Optional[int] = None,
|
||||||
required: bool = False,
|
required: bool = False,
|
||||||
|
negative: bool = False,
|
||||||
) -> Optional[int]: ...
|
) -> Optional[int]: ...
|
||||||
|
|
||||||
|
|
||||||
|
@ -128,6 +164,7 @@ def parse_integer_from_args(
|
||||||
name: str,
|
name: str,
|
||||||
default: Optional[int] = None,
|
default: Optional[int] = None,
|
||||||
required: bool = False,
|
required: bool = False,
|
||||||
|
negative: bool = True,
|
||||||
) -> Optional[int]:
|
) -> Optional[int]:
|
||||||
"""Parse an integer parameter from the request string
|
"""Parse an integer parameter from the request string
|
||||||
|
|
||||||
|
@ -137,33 +174,37 @@ def parse_integer_from_args(
|
||||||
default: value to use if the parameter is absent, defaults to None.
|
default: value to use if the parameter is absent, defaults to None.
|
||||||
required: whether to raise a 400 SynapseError if the parameter is absent,
|
required: whether to raise a 400 SynapseError if the parameter is absent,
|
||||||
defaults to False.
|
defaults to False.
|
||||||
|
negative: whether to allow negative integers, defaults to True.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
An int value or the default.
|
An int value or the default.
|
||||||
|
|
||||||
Raises:
|
Raises:
|
||||||
SynapseError: if the parameter is absent and required, or if the
|
SynapseError: if the parameter is absent and required, if the
|
||||||
parameter is present and not an integer.
|
parameter is present and not an integer, or if the
|
||||||
|
parameter is illegitimate negative.
|
||||||
"""
|
"""
|
||||||
name_bytes = name.encode("ascii")
|
name_bytes = name.encode("ascii")
|
||||||
|
|
||||||
if name_bytes in args:
|
if name_bytes not in args:
|
||||||
try:
|
if not required:
|
||||||
return int(args[name_bytes][0])
|
|
||||||
except Exception:
|
|
||||||
message = "Query parameter %r must be an integer" % (name,)
|
|
||||||
raise SynapseError(
|
|
||||||
HTTPStatus.BAD_REQUEST, message, errcode=Codes.INVALID_PARAM
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
if required:
|
|
||||||
message = "Missing integer query parameter %r" % (name,)
|
|
||||||
raise SynapseError(
|
|
||||||
HTTPStatus.BAD_REQUEST, message, errcode=Codes.MISSING_PARAM
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
return default
|
return default
|
||||||
|
|
||||||
|
message = f"Missing required integer query parameter {name}"
|
||||||
|
raise SynapseError(HTTPStatus.BAD_REQUEST, message, errcode=Codes.MISSING_PARAM)
|
||||||
|
|
||||||
|
try:
|
||||||
|
integer = int(args[name_bytes][0])
|
||||||
|
except Exception:
|
||||||
|
message = f"Query parameter {name} must be an integer"
|
||||||
|
raise SynapseError(HTTPStatus.BAD_REQUEST, message, errcode=Codes.INVALID_PARAM)
|
||||||
|
|
||||||
|
if not negative and integer < 0:
|
||||||
|
message = f"Query parameter {name} must be a positive integer."
|
||||||
|
raise SynapseError(HTTPStatus.BAD_REQUEST, message, errcode=Codes.INVALID_PARAM)
|
||||||
|
|
||||||
|
return integer
|
||||||
|
|
||||||
|
|
||||||
@overload
|
@overload
|
||||||
def parse_boolean(request: Request, name: str, default: bool) -> bool: ...
|
def parse_boolean(request: Request, name: str, default: bool) -> bool: ...
|
||||||
|
@ -410,6 +451,87 @@ def parse_string(
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def parse_json(
|
||||||
|
request: Request,
|
||||||
|
name: str,
|
||||||
|
default: Optional[dict] = None,
|
||||||
|
required: bool = False,
|
||||||
|
encoding: str = "ascii",
|
||||||
|
) -> Optional[JsonDict]:
|
||||||
|
"""
|
||||||
|
Parse a JSON parameter from the request query string.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
request: the twisted HTTP request.
|
||||||
|
name: the name of the query parameter.
|
||||||
|
default: value to use if the parameter is absent,
|
||||||
|
defaults to None.
|
||||||
|
required: whether to raise a 400 SynapseError if the
|
||||||
|
parameter is absent, defaults to False.
|
||||||
|
encoding: The encoding to decode the string content with.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
A JSON value, or `default` if the named query parameter was not found
|
||||||
|
and `required` was False.
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
SynapseError if the parameter is absent and required, or if the
|
||||||
|
parameter is present and not a JSON object.
|
||||||
|
"""
|
||||||
|
args: Mapping[bytes, Sequence[bytes]] = request.args # type: ignore
|
||||||
|
return parse_json_from_args(
|
||||||
|
args,
|
||||||
|
name,
|
||||||
|
default,
|
||||||
|
required=required,
|
||||||
|
encoding=encoding,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def parse_json_from_args(
|
||||||
|
args: Mapping[bytes, Sequence[bytes]],
|
||||||
|
name: str,
|
||||||
|
default: Optional[dict] = None,
|
||||||
|
required: bool = False,
|
||||||
|
encoding: str = "ascii",
|
||||||
|
) -> Optional[JsonDict]:
|
||||||
|
"""
|
||||||
|
Parse a JSON parameter from the request query string.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
args: a mapping of request args as bytes to a list of bytes (e.g. request.args).
|
||||||
|
name: the name of the query parameter.
|
||||||
|
default: value to use if the parameter is absent,
|
||||||
|
defaults to None.
|
||||||
|
required: whether to raise a 400 SynapseError if the
|
||||||
|
parameter is absent, defaults to False.
|
||||||
|
encoding: the encoding to decode the string content with.
|
||||||
|
|
||||||
|
A JSON value, or `default` if the named query parameter was not found
|
||||||
|
and `required` was False.
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
SynapseError if the parameter is absent and required, or if the
|
||||||
|
parameter is present and not a JSON object.
|
||||||
|
"""
|
||||||
|
name_bytes = name.encode("ascii")
|
||||||
|
|
||||||
|
if name_bytes not in args:
|
||||||
|
if not required:
|
||||||
|
return default
|
||||||
|
|
||||||
|
message = f"Missing required integer query parameter {name}"
|
||||||
|
raise SynapseError(HTTPStatus.BAD_REQUEST, message, errcode=Codes.MISSING_PARAM)
|
||||||
|
|
||||||
|
json_str = parse_string_from_args(args, name, required=True, encoding=encoding)
|
||||||
|
|
||||||
|
try:
|
||||||
|
return json_decoder.decode(urlparse.unquote(json_str))
|
||||||
|
except Exception:
|
||||||
|
message = f"Query parameter {name} must be a valid JSON object"
|
||||||
|
raise SynapseError(HTTPStatus.BAD_REQUEST, message, errcode=Codes.NOT_JSON)
|
||||||
|
|
||||||
|
|
||||||
EnumT = TypeVar("EnumT", bound=enum.Enum)
|
EnumT = TypeVar("EnumT", bound=enum.Enum)
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -150,7 +150,8 @@ class SynapseRequest(Request):
|
||||||
self.get_method(),
|
self.get_method(),
|
||||||
self.get_redacted_uri(),
|
self.get_redacted_uri(),
|
||||||
)
|
)
|
||||||
self.transport.abortConnection()
|
if self.channel:
|
||||||
|
self.channel.forceAbortClient()
|
||||||
return
|
return
|
||||||
super().handleContentChunk(data)
|
super().handleContentChunk(data)
|
||||||
|
|
||||||
|
|
|
@ -64,6 +64,7 @@ INLINE_CONTENT_TYPES = [
|
||||||
"text/csv",
|
"text/csv",
|
||||||
"application/json",
|
"application/json",
|
||||||
"application/ld+json",
|
"application/ld+json",
|
||||||
|
"application/pdf",
|
||||||
# We allow some media files deemed as safe, which comes from the matrix-react-sdk.
|
# We allow some media files deemed as safe, which comes from the matrix-react-sdk.
|
||||||
# https://github.com/matrix-org/matrix-react-sdk/blob/a70fcfd0bcf7f8c85986da18001ea11597989a7c/src/utils/blobs.ts#L51
|
# https://github.com/matrix-org/matrix-react-sdk/blob/a70fcfd0bcf7f8c85986da18001ea11597989a7c/src/utils/blobs.ts#L51
|
||||||
# SVGs are *intentionally* omitted.
|
# SVGs are *intentionally* omitted.
|
||||||
|
@ -206,7 +207,9 @@ def add_file_headers(
|
||||||
# recommend caching as it's sensitive or private - or at least
|
# recommend caching as it's sensitive or private - or at least
|
||||||
# select private. don't bother setting Expires as all our
|
# select private. don't bother setting Expires as all our
|
||||||
# clients are smart enough to be happy with Cache-Control
|
# clients are smart enough to be happy with Cache-Control
|
||||||
request.setHeader(b"Cache-Control", b"public,max-age=86400,s-maxage=86400")
|
request.setHeader(
|
||||||
|
b"Cache-Control", b"public,immutable,max-age=86400,s-maxage=86400"
|
||||||
|
)
|
||||||
if file_size is not None:
|
if file_size is not None:
|
||||||
request.setHeader(b"Content-Length", b"%d" % (file_size,))
|
request.setHeader(b"Content-Length", b"%d" % (file_size,))
|
||||||
|
|
||||||
|
|
|
@ -650,7 +650,7 @@ class MediaRepository:
|
||||||
|
|
||||||
file_info = FileInfo(server_name=server_name, file_id=file_id)
|
file_info = FileInfo(server_name=server_name, file_id=file_id)
|
||||||
|
|
||||||
with self.media_storage.store_into_file(file_info) as (f, fname, finish):
|
async with self.media_storage.store_into_file(file_info) as (f, fname):
|
||||||
try:
|
try:
|
||||||
length, headers = await self.client.download_media(
|
length, headers = await self.client.download_media(
|
||||||
server_name,
|
server_name,
|
||||||
|
@ -693,8 +693,6 @@ class MediaRepository:
|
||||||
)
|
)
|
||||||
raise SynapseError(502, "Failed to fetch remote media")
|
raise SynapseError(502, "Failed to fetch remote media")
|
||||||
|
|
||||||
await finish()
|
|
||||||
|
|
||||||
if b"Content-Type" in headers:
|
if b"Content-Type" in headers:
|
||||||
media_type = headers[b"Content-Type"][0].decode("ascii")
|
media_type = headers[b"Content-Type"][0].decode("ascii")
|
||||||
else:
|
else:
|
||||||
|
@ -1045,17 +1043,17 @@ class MediaRepository:
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
with self.media_storage.store_into_file(file_info) as (
|
async with self.media_storage.store_into_file(file_info) as (f, fname):
|
||||||
f,
|
|
||||||
fname,
|
|
||||||
finish,
|
|
||||||
):
|
|
||||||
try:
|
try:
|
||||||
await self.media_storage.write_to_file(t_byte_source, f)
|
await self.media_storage.write_to_file(t_byte_source, f)
|
||||||
await finish()
|
|
||||||
finally:
|
finally:
|
||||||
t_byte_source.close()
|
t_byte_source.close()
|
||||||
|
|
||||||
|
# We flush and close the file to ensure that the bytes have
|
||||||
|
# been written before getting the size.
|
||||||
|
f.flush()
|
||||||
|
f.close()
|
||||||
|
|
||||||
t_len = os.path.getsize(fname)
|
t_len = os.path.getsize(fname)
|
||||||
|
|
||||||
# Write to database
|
# Write to database
|
||||||
|
|
|
@ -27,10 +27,9 @@ from typing import (
|
||||||
IO,
|
IO,
|
||||||
TYPE_CHECKING,
|
TYPE_CHECKING,
|
||||||
Any,
|
Any,
|
||||||
Awaitable,
|
AsyncIterator,
|
||||||
BinaryIO,
|
BinaryIO,
|
||||||
Callable,
|
Callable,
|
||||||
Generator,
|
|
||||||
Optional,
|
Optional,
|
||||||
Sequence,
|
Sequence,
|
||||||
Tuple,
|
Tuple,
|
||||||
|
@ -97,11 +96,9 @@ class MediaStorage:
|
||||||
the file path written to in the primary media store
|
the file path written to in the primary media store
|
||||||
"""
|
"""
|
||||||
|
|
||||||
with self.store_into_file(file_info) as (f, fname, finish_cb):
|
async with self.store_into_file(file_info) as (f, fname):
|
||||||
# Write to the main media repository
|
# Write to the main media repository
|
||||||
await self.write_to_file(source, f)
|
await self.write_to_file(source, f)
|
||||||
# Write to the other storage providers
|
|
||||||
await finish_cb()
|
|
||||||
|
|
||||||
return fname
|
return fname
|
||||||
|
|
||||||
|
@ -111,32 +108,27 @@ class MediaStorage:
|
||||||
await defer_to_thread(self.reactor, _write_file_synchronously, source, output)
|
await defer_to_thread(self.reactor, _write_file_synchronously, source, output)
|
||||||
|
|
||||||
@trace_with_opname("MediaStorage.store_into_file")
|
@trace_with_opname("MediaStorage.store_into_file")
|
||||||
@contextlib.contextmanager
|
@contextlib.asynccontextmanager
|
||||||
def store_into_file(
|
async def store_into_file(
|
||||||
self, file_info: FileInfo
|
self, file_info: FileInfo
|
||||||
) -> Generator[Tuple[BinaryIO, str, Callable[[], Awaitable[None]]], None, None]:
|
) -> AsyncIterator[Tuple[BinaryIO, str]]:
|
||||||
"""Context manager used to get a file like object to write into, as
|
"""Async Context manager used to get a file like object to write into, as
|
||||||
described by file_info.
|
described by file_info.
|
||||||
|
|
||||||
Actually yields a 3-tuple (file, fname, finish_cb), where file is a file
|
Actually yields a 2-tuple (file, fname,), where file is a file
|
||||||
like object that can be written to, fname is the absolute path of file
|
like object that can be written to and fname is the absolute path of file
|
||||||
on disk, and finish_cb is a function that returns an awaitable.
|
on disk.
|
||||||
|
|
||||||
fname can be used to read the contents from after upload, e.g. to
|
fname can be used to read the contents from after upload, e.g. to
|
||||||
generate thumbnails.
|
generate thumbnails.
|
||||||
|
|
||||||
finish_cb must be called and waited on after the file has been successfully been
|
|
||||||
written to. Should not be called if there was an error. Checks for spam and
|
|
||||||
stores the file into the configured storage providers.
|
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
file_info: Info about the file to store
|
file_info: Info about the file to store
|
||||||
|
|
||||||
Example:
|
Example:
|
||||||
|
|
||||||
with media_storage.store_into_file(info) as (f, fname, finish_cb):
|
async with media_storage.store_into_file(info) as (f, fname,):
|
||||||
# .. write into f ...
|
# .. write into f ...
|
||||||
await finish_cb()
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
path = self._file_info_to_path(file_info)
|
path = self._file_info_to_path(file_info)
|
||||||
|
@ -145,63 +137,38 @@ class MediaStorage:
|
||||||
dirname = os.path.dirname(fname)
|
dirname = os.path.dirname(fname)
|
||||||
os.makedirs(dirname, exist_ok=True)
|
os.makedirs(dirname, exist_ok=True)
|
||||||
|
|
||||||
finished_called = [False]
|
|
||||||
|
|
||||||
main_media_repo_write_trace_scope = start_active_span(
|
|
||||||
"writing to main media repo"
|
|
||||||
)
|
|
||||||
main_media_repo_write_trace_scope.__enter__()
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
with open(fname, "wb") as f:
|
with start_active_span("writing to main media repo"):
|
||||||
|
with open(fname, "wb") as f:
|
||||||
|
yield f, fname
|
||||||
|
|
||||||
async def finish() -> None:
|
with start_active_span("writing to other storage providers"):
|
||||||
# When someone calls finish, we assume they are done writing to the main media repo
|
spam_check = (
|
||||||
main_media_repo_write_trace_scope.__exit__(None, None, None)
|
await self._spam_checker_module_callbacks.check_media_file_for_spam(
|
||||||
|
ReadableFileWrapper(self.clock, fname), file_info
|
||||||
|
)
|
||||||
|
)
|
||||||
|
if spam_check != self._spam_checker_module_callbacks.NOT_SPAM:
|
||||||
|
logger.info("Blocking media due to spam checker")
|
||||||
|
# Note that we'll delete the stored media, due to the
|
||||||
|
# try/except below. The media also won't be stored in
|
||||||
|
# the DB.
|
||||||
|
# We currently ignore any additional field returned by
|
||||||
|
# the spam-check API.
|
||||||
|
raise SpamMediaException(errcode=spam_check[0])
|
||||||
|
|
||||||
with start_active_span("writing to other storage providers"):
|
for provider in self.storage_providers:
|
||||||
# Ensure that all writes have been flushed and close the
|
with start_active_span(str(provider)):
|
||||||
# file.
|
await provider.store_file(path, file_info)
|
||||||
f.flush()
|
|
||||||
f.close()
|
|
||||||
|
|
||||||
spam_check = await self._spam_checker_module_callbacks.check_media_file_for_spam(
|
|
||||||
ReadableFileWrapper(self.clock, fname), file_info
|
|
||||||
)
|
|
||||||
if spam_check != self._spam_checker_module_callbacks.NOT_SPAM:
|
|
||||||
logger.info("Blocking media due to spam checker")
|
|
||||||
# Note that we'll delete the stored media, due to the
|
|
||||||
# try/except below. The media also won't be stored in
|
|
||||||
# the DB.
|
|
||||||
# We currently ignore any additional field returned by
|
|
||||||
# the spam-check API.
|
|
||||||
raise SpamMediaException(errcode=spam_check[0])
|
|
||||||
|
|
||||||
for provider in self.storage_providers:
|
|
||||||
with start_active_span(str(provider)):
|
|
||||||
await provider.store_file(path, file_info)
|
|
||||||
|
|
||||||
finished_called[0] = True
|
|
||||||
|
|
||||||
yield f, fname, finish
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
try:
|
try:
|
||||||
main_media_repo_write_trace_scope.__exit__(
|
|
||||||
type(e), None, e.__traceback__
|
|
||||||
)
|
|
||||||
os.remove(fname)
|
os.remove(fname)
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
raise e from None
|
raise e from None
|
||||||
|
|
||||||
if not finished_called:
|
|
||||||
exc = Exception("Finished callback not called")
|
|
||||||
main_media_repo_write_trace_scope.__exit__(
|
|
||||||
type(exc), None, exc.__traceback__
|
|
||||||
)
|
|
||||||
raise exc
|
|
||||||
|
|
||||||
async def fetch_media(self, file_info: FileInfo) -> Optional[Responder]:
|
async def fetch_media(self, file_info: FileInfo) -> Optional[Responder]:
|
||||||
"""Attempts to fetch media described by file_info from the local cache
|
"""Attempts to fetch media described by file_info from the local cache
|
||||||
and configured storage providers.
|
and configured storage providers.
|
||||||
|
|
|
@ -22,11 +22,27 @@
|
||||||
import logging
|
import logging
|
||||||
from io import BytesIO
|
from io import BytesIO
|
||||||
from types import TracebackType
|
from types import TracebackType
|
||||||
from typing import Optional, Tuple, Type
|
from typing import TYPE_CHECKING, List, Optional, Tuple, Type
|
||||||
|
|
||||||
from PIL import Image
|
from PIL import Image
|
||||||
|
|
||||||
|
from synapse.api.errors import Codes, SynapseError, cs_error
|
||||||
|
from synapse.config.repository import THUMBNAIL_SUPPORTED_MEDIA_FORMAT_MAP
|
||||||
|
from synapse.http.server import respond_with_json
|
||||||
|
from synapse.http.site import SynapseRequest
|
||||||
from synapse.logging.opentracing import trace
|
from synapse.logging.opentracing import trace
|
||||||
|
from synapse.media._base import (
|
||||||
|
FileInfo,
|
||||||
|
ThumbnailInfo,
|
||||||
|
respond_404,
|
||||||
|
respond_with_file,
|
||||||
|
respond_with_responder,
|
||||||
|
)
|
||||||
|
from synapse.media.media_storage import MediaStorage
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from synapse.media.media_repository import MediaRepository
|
||||||
|
from synapse.server import HomeServer
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
@ -47,7 +63,7 @@ class ThumbnailError(Exception):
|
||||||
|
|
||||||
|
|
||||||
class Thumbnailer:
|
class Thumbnailer:
|
||||||
FORMATS = {"image/jpeg": "JPEG", "image/png": "PNG"}
|
FORMATS = {"image/jpeg": "JPEG", "image/png": "PNG", "image/webp": "WEBP"}
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def set_limits(max_image_pixels: int) -> None:
|
def set_limits(max_image_pixels: int) -> None:
|
||||||
|
@ -231,3 +247,471 @@ class Thumbnailer:
|
||||||
def __del__(self) -> None:
|
def __del__(self) -> None:
|
||||||
# Make sure we actually do close the image, rather than leak data.
|
# Make sure we actually do close the image, rather than leak data.
|
||||||
self.close()
|
self.close()
|
||||||
|
|
||||||
|
|
||||||
|
class ThumbnailProvider:
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
hs: "HomeServer",
|
||||||
|
media_repo: "MediaRepository",
|
||||||
|
media_storage: MediaStorage,
|
||||||
|
):
|
||||||
|
self.hs = hs
|
||||||
|
self.media_repo = media_repo
|
||||||
|
self.media_storage = media_storage
|
||||||
|
self.store = hs.get_datastores().main
|
||||||
|
self.dynamic_thumbnails = hs.config.media.dynamic_thumbnails
|
||||||
|
|
||||||
|
async def respond_local_thumbnail(
|
||||||
|
self,
|
||||||
|
request: SynapseRequest,
|
||||||
|
media_id: str,
|
||||||
|
width: int,
|
||||||
|
height: int,
|
||||||
|
method: str,
|
||||||
|
m_type: str,
|
||||||
|
max_timeout_ms: int,
|
||||||
|
) -> None:
|
||||||
|
media_info = await self.media_repo.get_local_media_info(
|
||||||
|
request, media_id, max_timeout_ms
|
||||||
|
)
|
||||||
|
if not media_info:
|
||||||
|
return
|
||||||
|
|
||||||
|
thumbnail_infos = await self.store.get_local_media_thumbnails(media_id)
|
||||||
|
await self._select_and_respond_with_thumbnail(
|
||||||
|
request,
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
method,
|
||||||
|
m_type,
|
||||||
|
thumbnail_infos,
|
||||||
|
media_id,
|
||||||
|
media_id,
|
||||||
|
url_cache=bool(media_info.url_cache),
|
||||||
|
server_name=None,
|
||||||
|
)
|
||||||
|
|
||||||
|
async def select_or_generate_local_thumbnail(
|
||||||
|
self,
|
||||||
|
request: SynapseRequest,
|
||||||
|
media_id: str,
|
||||||
|
desired_width: int,
|
||||||
|
desired_height: int,
|
||||||
|
desired_method: str,
|
||||||
|
desired_type: str,
|
||||||
|
max_timeout_ms: int,
|
||||||
|
) -> None:
|
||||||
|
media_info = await self.media_repo.get_local_media_info(
|
||||||
|
request, media_id, max_timeout_ms
|
||||||
|
)
|
||||||
|
|
||||||
|
if not media_info:
|
||||||
|
return
|
||||||
|
|
||||||
|
thumbnail_infos = await self.store.get_local_media_thumbnails(media_id)
|
||||||
|
for info in thumbnail_infos:
|
||||||
|
t_w = info.width == desired_width
|
||||||
|
t_h = info.height == desired_height
|
||||||
|
t_method = info.method == desired_method
|
||||||
|
t_type = info.type == desired_type
|
||||||
|
|
||||||
|
if t_w and t_h and t_method and t_type:
|
||||||
|
file_info = FileInfo(
|
||||||
|
server_name=None,
|
||||||
|
file_id=media_id,
|
||||||
|
url_cache=bool(media_info.url_cache),
|
||||||
|
thumbnail=info,
|
||||||
|
)
|
||||||
|
|
||||||
|
responder = await self.media_storage.fetch_media(file_info)
|
||||||
|
if responder:
|
||||||
|
await respond_with_responder(
|
||||||
|
request, responder, info.type, info.length
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
|
logger.debug("We don't have a thumbnail of that size. Generating")
|
||||||
|
|
||||||
|
# Okay, so we generate one.
|
||||||
|
file_path = await self.media_repo.generate_local_exact_thumbnail(
|
||||||
|
media_id,
|
||||||
|
desired_width,
|
||||||
|
desired_height,
|
||||||
|
desired_method,
|
||||||
|
desired_type,
|
||||||
|
url_cache=bool(media_info.url_cache),
|
||||||
|
)
|
||||||
|
|
||||||
|
if file_path:
|
||||||
|
await respond_with_file(request, desired_type, file_path)
|
||||||
|
else:
|
||||||
|
logger.warning("Failed to generate thumbnail")
|
||||||
|
raise SynapseError(400, "Failed to generate thumbnail.")
|
||||||
|
|
||||||
|
async def select_or_generate_remote_thumbnail(
|
||||||
|
self,
|
||||||
|
request: SynapseRequest,
|
||||||
|
server_name: str,
|
||||||
|
media_id: str,
|
||||||
|
desired_width: int,
|
||||||
|
desired_height: int,
|
||||||
|
desired_method: str,
|
||||||
|
desired_type: str,
|
||||||
|
max_timeout_ms: int,
|
||||||
|
) -> None:
|
||||||
|
media_info = await self.media_repo.get_remote_media_info(
|
||||||
|
server_name, media_id, max_timeout_ms
|
||||||
|
)
|
||||||
|
if not media_info:
|
||||||
|
respond_404(request)
|
||||||
|
return
|
||||||
|
|
||||||
|
thumbnail_infos = await self.store.get_remote_media_thumbnails(
|
||||||
|
server_name, media_id
|
||||||
|
)
|
||||||
|
|
||||||
|
file_id = media_info.filesystem_id
|
||||||
|
|
||||||
|
for info in thumbnail_infos:
|
||||||
|
t_w = info.width == desired_width
|
||||||
|
t_h = info.height == desired_height
|
||||||
|
t_method = info.method == desired_method
|
||||||
|
t_type = info.type == desired_type
|
||||||
|
|
||||||
|
if t_w and t_h and t_method and t_type:
|
||||||
|
file_info = FileInfo(
|
||||||
|
server_name=server_name,
|
||||||
|
file_id=file_id,
|
||||||
|
thumbnail=info,
|
||||||
|
)
|
||||||
|
|
||||||
|
responder = await self.media_storage.fetch_media(file_info)
|
||||||
|
if responder:
|
||||||
|
await respond_with_responder(
|
||||||
|
request, responder, info.type, info.length
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
|
logger.debug("We don't have a thumbnail of that size. Generating")
|
||||||
|
|
||||||
|
# Okay, so we generate one.
|
||||||
|
file_path = await self.media_repo.generate_remote_exact_thumbnail(
|
||||||
|
server_name,
|
||||||
|
file_id,
|
||||||
|
media_id,
|
||||||
|
desired_width,
|
||||||
|
desired_height,
|
||||||
|
desired_method,
|
||||||
|
desired_type,
|
||||||
|
)
|
||||||
|
|
||||||
|
if file_path:
|
||||||
|
await respond_with_file(request, desired_type, file_path)
|
||||||
|
else:
|
||||||
|
logger.warning("Failed to generate thumbnail")
|
||||||
|
raise SynapseError(400, "Failed to generate thumbnail.")
|
||||||
|
|
||||||
|
async def respond_remote_thumbnail(
|
||||||
|
self,
|
||||||
|
request: SynapseRequest,
|
||||||
|
server_name: str,
|
||||||
|
media_id: str,
|
||||||
|
width: int,
|
||||||
|
height: int,
|
||||||
|
method: str,
|
||||||
|
m_type: str,
|
||||||
|
max_timeout_ms: int,
|
||||||
|
) -> None:
|
||||||
|
# TODO: Don't download the whole remote file
|
||||||
|
# We should proxy the thumbnail from the remote server instead of
|
||||||
|
# downloading the remote file and generating our own thumbnails.
|
||||||
|
media_info = await self.media_repo.get_remote_media_info(
|
||||||
|
server_name, media_id, max_timeout_ms
|
||||||
|
)
|
||||||
|
if not media_info:
|
||||||
|
return
|
||||||
|
|
||||||
|
thumbnail_infos = await self.store.get_remote_media_thumbnails(
|
||||||
|
server_name, media_id
|
||||||
|
)
|
||||||
|
await self._select_and_respond_with_thumbnail(
|
||||||
|
request,
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
method,
|
||||||
|
m_type,
|
||||||
|
thumbnail_infos,
|
||||||
|
media_id,
|
||||||
|
media_info.filesystem_id,
|
||||||
|
url_cache=False,
|
||||||
|
server_name=server_name,
|
||||||
|
)
|
||||||
|
|
||||||
|
async def _select_and_respond_with_thumbnail(
|
||||||
|
self,
|
||||||
|
request: SynapseRequest,
|
||||||
|
desired_width: int,
|
||||||
|
desired_height: int,
|
||||||
|
desired_method: str,
|
||||||
|
desired_type: str,
|
||||||
|
thumbnail_infos: List[ThumbnailInfo],
|
||||||
|
media_id: str,
|
||||||
|
file_id: str,
|
||||||
|
url_cache: bool,
|
||||||
|
server_name: Optional[str] = None,
|
||||||
|
) -> None:
|
||||||
|
"""
|
||||||
|
Respond to a request with an appropriate thumbnail from the previously generated thumbnails.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
request: The incoming request.
|
||||||
|
desired_width: The desired width, the returned thumbnail may be larger than this.
|
||||||
|
desired_height: The desired height, the returned thumbnail may be larger than this.
|
||||||
|
desired_method: The desired method used to generate the thumbnail.
|
||||||
|
desired_type: The desired content-type of the thumbnail.
|
||||||
|
thumbnail_infos: A list of thumbnail info of candidate thumbnails.
|
||||||
|
file_id: The ID of the media that a thumbnail is being requested for.
|
||||||
|
url_cache: True if this is from a URL cache.
|
||||||
|
server_name: The server name, if this is a remote thumbnail.
|
||||||
|
"""
|
||||||
|
logger.debug(
|
||||||
|
"_select_and_respond_with_thumbnail: media_id=%s desired=%sx%s (%s) thumbnail_infos=%s",
|
||||||
|
media_id,
|
||||||
|
desired_width,
|
||||||
|
desired_height,
|
||||||
|
desired_method,
|
||||||
|
thumbnail_infos,
|
||||||
|
)
|
||||||
|
|
||||||
|
# If `dynamic_thumbnails` is enabled, we expect Synapse to go down a
|
||||||
|
# different code path to handle it.
|
||||||
|
assert not self.dynamic_thumbnails
|
||||||
|
|
||||||
|
if thumbnail_infos:
|
||||||
|
file_info = self._select_thumbnail(
|
||||||
|
desired_width,
|
||||||
|
desired_height,
|
||||||
|
desired_method,
|
||||||
|
desired_type,
|
||||||
|
thumbnail_infos,
|
||||||
|
file_id,
|
||||||
|
url_cache,
|
||||||
|
server_name,
|
||||||
|
)
|
||||||
|
if not file_info:
|
||||||
|
logger.info("Couldn't find a thumbnail matching the desired inputs")
|
||||||
|
respond_404(request)
|
||||||
|
return
|
||||||
|
|
||||||
|
# The thumbnail property must exist.
|
||||||
|
assert file_info.thumbnail is not None
|
||||||
|
|
||||||
|
responder = await self.media_storage.fetch_media(file_info)
|
||||||
|
if responder:
|
||||||
|
await respond_with_responder(
|
||||||
|
request,
|
||||||
|
responder,
|
||||||
|
file_info.thumbnail.type,
|
||||||
|
file_info.thumbnail.length,
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
|
# If we can't find the thumbnail we regenerate it. This can happen
|
||||||
|
# if e.g. we've deleted the thumbnails but still have the original
|
||||||
|
# image somewhere.
|
||||||
|
#
|
||||||
|
# Since we have an entry for the thumbnail in the DB we a) know we
|
||||||
|
# have have successfully generated the thumbnail in the past (so we
|
||||||
|
# don't need to worry about repeatedly failing to generate
|
||||||
|
# thumbnails), and b) have already calculated that appropriate
|
||||||
|
# width/height/method so we can just call the "generate exact"
|
||||||
|
# methods.
|
||||||
|
|
||||||
|
# First let's check that we do actually have the original image
|
||||||
|
# still. This will throw a 404 if we don't.
|
||||||
|
# TODO: We should refetch the thumbnails for remote media.
|
||||||
|
await self.media_storage.ensure_media_is_in_local_cache(
|
||||||
|
FileInfo(server_name, file_id, url_cache=url_cache)
|
||||||
|
)
|
||||||
|
|
||||||
|
if server_name:
|
||||||
|
await self.media_repo.generate_remote_exact_thumbnail(
|
||||||
|
server_name,
|
||||||
|
file_id=file_id,
|
||||||
|
media_id=media_id,
|
||||||
|
t_width=file_info.thumbnail.width,
|
||||||
|
t_height=file_info.thumbnail.height,
|
||||||
|
t_method=file_info.thumbnail.method,
|
||||||
|
t_type=file_info.thumbnail.type,
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
await self.media_repo.generate_local_exact_thumbnail(
|
||||||
|
media_id=media_id,
|
||||||
|
t_width=file_info.thumbnail.width,
|
||||||
|
t_height=file_info.thumbnail.height,
|
||||||
|
t_method=file_info.thumbnail.method,
|
||||||
|
t_type=file_info.thumbnail.type,
|
||||||
|
url_cache=url_cache,
|
||||||
|
)
|
||||||
|
|
||||||
|
responder = await self.media_storage.fetch_media(file_info)
|
||||||
|
await respond_with_responder(
|
||||||
|
request,
|
||||||
|
responder,
|
||||||
|
file_info.thumbnail.type,
|
||||||
|
file_info.thumbnail.length,
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
# This might be because:
|
||||||
|
# 1. We can't create thumbnails for the given media (corrupted or
|
||||||
|
# unsupported file type), or
|
||||||
|
# 2. The thumbnailing process never ran or errored out initially
|
||||||
|
# when the media was first uploaded (these bugs should be
|
||||||
|
# reported and fixed).
|
||||||
|
# Note that we don't attempt to generate a thumbnail now because
|
||||||
|
# `dynamic_thumbnails` is disabled.
|
||||||
|
logger.info("Failed to find any generated thumbnails")
|
||||||
|
|
||||||
|
assert request.path is not None
|
||||||
|
respond_with_json(
|
||||||
|
request,
|
||||||
|
400,
|
||||||
|
cs_error(
|
||||||
|
"Cannot find any thumbnails for the requested media ('%s'). This might mean the media is not a supported_media_format=(%s) or that thumbnailing failed for some other reason. (Dynamic thumbnails are disabled on this server.)"
|
||||||
|
% (
|
||||||
|
request.path.decode(),
|
||||||
|
", ".join(THUMBNAIL_SUPPORTED_MEDIA_FORMAT_MAP.keys()),
|
||||||
|
),
|
||||||
|
code=Codes.UNKNOWN,
|
||||||
|
),
|
||||||
|
send_cors=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
def _select_thumbnail(
|
||||||
|
self,
|
||||||
|
desired_width: int,
|
||||||
|
desired_height: int,
|
||||||
|
desired_method: str,
|
||||||
|
desired_type: str,
|
||||||
|
thumbnail_infos: List[ThumbnailInfo],
|
||||||
|
file_id: str,
|
||||||
|
url_cache: bool,
|
||||||
|
server_name: Optional[str],
|
||||||
|
) -> Optional[FileInfo]:
|
||||||
|
"""
|
||||||
|
Choose an appropriate thumbnail from the previously generated thumbnails.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
desired_width: The desired width, the returned thumbnail may be larger than this.
|
||||||
|
desired_height: The desired height, the returned thumbnail may be larger than this.
|
||||||
|
desired_method: The desired method used to generate the thumbnail.
|
||||||
|
desired_type: The desired content-type of the thumbnail.
|
||||||
|
thumbnail_infos: A list of thumbnail infos of candidate thumbnails.
|
||||||
|
file_id: The ID of the media that a thumbnail is being requested for.
|
||||||
|
url_cache: True if this is from a URL cache.
|
||||||
|
server_name: The server name, if this is a remote thumbnail.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
The thumbnail which best matches the desired parameters.
|
||||||
|
"""
|
||||||
|
desired_method = desired_method.lower()
|
||||||
|
|
||||||
|
# The chosen thumbnail.
|
||||||
|
thumbnail_info = None
|
||||||
|
|
||||||
|
d_w = desired_width
|
||||||
|
d_h = desired_height
|
||||||
|
|
||||||
|
if desired_method == "crop":
|
||||||
|
# Thumbnails that match equal or larger sizes of desired width/height.
|
||||||
|
crop_info_list: List[
|
||||||
|
Tuple[int, int, int, bool, Optional[int], ThumbnailInfo]
|
||||||
|
] = []
|
||||||
|
# Other thumbnails.
|
||||||
|
crop_info_list2: List[
|
||||||
|
Tuple[int, int, int, bool, Optional[int], ThumbnailInfo]
|
||||||
|
] = []
|
||||||
|
for info in thumbnail_infos:
|
||||||
|
# Skip thumbnails generated with different methods.
|
||||||
|
if info.method != "crop":
|
||||||
|
continue
|
||||||
|
|
||||||
|
t_w = info.width
|
||||||
|
t_h = info.height
|
||||||
|
aspect_quality = abs(d_w * t_h - d_h * t_w)
|
||||||
|
min_quality = 0 if d_w <= t_w and d_h <= t_h else 1
|
||||||
|
size_quality = abs((d_w - t_w) * (d_h - t_h))
|
||||||
|
type_quality = desired_type != info.type
|
||||||
|
length_quality = info.length
|
||||||
|
if t_w >= d_w or t_h >= d_h:
|
||||||
|
crop_info_list.append(
|
||||||
|
(
|
||||||
|
aspect_quality,
|
||||||
|
min_quality,
|
||||||
|
size_quality,
|
||||||
|
type_quality,
|
||||||
|
length_quality,
|
||||||
|
info,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
crop_info_list2.append(
|
||||||
|
(
|
||||||
|
aspect_quality,
|
||||||
|
min_quality,
|
||||||
|
size_quality,
|
||||||
|
type_quality,
|
||||||
|
length_quality,
|
||||||
|
info,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
# Pick the most appropriate thumbnail. Some values of `desired_width` and
|
||||||
|
# `desired_height` may result in a tie, in which case we avoid comparing on
|
||||||
|
# the thumbnail info and pick the thumbnail that appears earlier
|
||||||
|
# in the list of candidates.
|
||||||
|
if crop_info_list:
|
||||||
|
thumbnail_info = min(crop_info_list, key=lambda t: t[:-1])[-1]
|
||||||
|
elif crop_info_list2:
|
||||||
|
thumbnail_info = min(crop_info_list2, key=lambda t: t[:-1])[-1]
|
||||||
|
elif desired_method == "scale":
|
||||||
|
# Thumbnails that match equal or larger sizes of desired width/height.
|
||||||
|
info_list: List[Tuple[int, bool, int, ThumbnailInfo]] = []
|
||||||
|
# Other thumbnails.
|
||||||
|
info_list2: List[Tuple[int, bool, int, ThumbnailInfo]] = []
|
||||||
|
|
||||||
|
for info in thumbnail_infos:
|
||||||
|
# Skip thumbnails generated with different methods.
|
||||||
|
if info.method != "scale":
|
||||||
|
continue
|
||||||
|
|
||||||
|
t_w = info.width
|
||||||
|
t_h = info.height
|
||||||
|
size_quality = abs((d_w - t_w) * (d_h - t_h))
|
||||||
|
type_quality = desired_type != info.type
|
||||||
|
length_quality = info.length
|
||||||
|
if t_w >= d_w or t_h >= d_h:
|
||||||
|
info_list.append((size_quality, type_quality, length_quality, info))
|
||||||
|
else:
|
||||||
|
info_list2.append(
|
||||||
|
(size_quality, type_quality, length_quality, info)
|
||||||
|
)
|
||||||
|
# Pick the most appropriate thumbnail. Some values of `desired_width` and
|
||||||
|
# `desired_height` may result in a tie, in which case we avoid comparing on
|
||||||
|
# the thumbnail info and pick the thumbnail that appears earlier
|
||||||
|
# in the list of candidates.
|
||||||
|
if info_list:
|
||||||
|
thumbnail_info = min(info_list, key=lambda t: t[:-1])[-1]
|
||||||
|
elif info_list2:
|
||||||
|
thumbnail_info = min(info_list2, key=lambda t: t[:-1])[-1]
|
||||||
|
|
||||||
|
if thumbnail_info:
|
||||||
|
return FileInfo(
|
||||||
|
file_id=file_id,
|
||||||
|
url_cache=url_cache,
|
||||||
|
server_name=server_name,
|
||||||
|
thumbnail=thumbnail_info,
|
||||||
|
)
|
||||||
|
|
||||||
|
# No matching thumbnail was found.
|
||||||
|
return None
|
||||||
|
|
|
@ -592,7 +592,7 @@ class UrlPreviewer:
|
||||||
|
|
||||||
file_info = FileInfo(server_name=None, file_id=file_id, url_cache=True)
|
file_info = FileInfo(server_name=None, file_id=file_id, url_cache=True)
|
||||||
|
|
||||||
with self.media_storage.store_into_file(file_info) as (f, fname, finish):
|
async with self.media_storage.store_into_file(file_info) as (f, fname):
|
||||||
if url.startswith("data:"):
|
if url.startswith("data:"):
|
||||||
if not allow_data_urls:
|
if not allow_data_urls:
|
||||||
raise SynapseError(
|
raise SynapseError(
|
||||||
|
@ -603,8 +603,6 @@ class UrlPreviewer:
|
||||||
else:
|
else:
|
||||||
download_result = await self._download_url(url, f)
|
download_result = await self._download_url(url, f)
|
||||||
|
|
||||||
await finish()
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
time_now_ms = self.clock.time_msec()
|
time_now_ms = self.clock.time_msec()
|
||||||
|
|
||||||
|
|
|
@ -721,6 +721,7 @@ class Notifier:
|
||||||
user.to_string(),
|
user.to_string(),
|
||||||
new_events,
|
new_events,
|
||||||
is_peeking=is_peeking,
|
is_peeking=is_peeking,
|
||||||
|
msc4115_membership_on_events=self.hs.config.experimental.msc4115_membership_on_events,
|
||||||
)
|
)
|
||||||
elif keyname == StreamKeyType.PRESENCE:
|
elif keyname == StreamKeyType.PRESENCE:
|
||||||
now = self.clock.time_msec()
|
now = self.clock.time_msec()
|
||||||
|
@ -762,6 +763,29 @@ class Notifier:
|
||||||
|
|
||||||
return result
|
return result
|
||||||
|
|
||||||
|
async def wait_for_stream_token(self, stream_token: StreamToken) -> bool:
|
||||||
|
"""Wait for this worker to catch up with the given stream token."""
|
||||||
|
|
||||||
|
start = self.clock.time_msec()
|
||||||
|
while True:
|
||||||
|
current_token = self.event_sources.get_current_token()
|
||||||
|
if stream_token.is_before_or_eq(current_token):
|
||||||
|
return True
|
||||||
|
|
||||||
|
now = self.clock.time_msec()
|
||||||
|
|
||||||
|
if now - start > 10_000:
|
||||||
|
return False
|
||||||
|
|
||||||
|
logger.info(
|
||||||
|
"Waiting for current token to reach %s; currently at %s",
|
||||||
|
stream_token,
|
||||||
|
current_token,
|
||||||
|
)
|
||||||
|
|
||||||
|
# TODO: be better
|
||||||
|
await self.clock.sleep(0.5)
|
||||||
|
|
||||||
async def _get_room_ids(
|
async def _get_room_ids(
|
||||||
self, user: UserID, explicit_room_id: Optional[str]
|
self, user: UserID, explicit_room_id: Optional[str]
|
||||||
) -> Tuple[StrCollection, bool]:
|
) -> Tuple[StrCollection, bool]:
|
||||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue