diff --git a/.github/workflows/complement_tests.yml b/.github/workflows/complement_tests.yml
index bc0e82b667..2773003725 100644
--- a/.github/workflows/complement_tests.yml
+++ b/.github/workflows/complement_tests.yml
@@ -58,7 +58,7 @@ jobs:
# We use `poetry` in `complement.sh`
- uses: matrix-org/setup-python-poetry@5bbf6603c5c930615ec8a29f1b5d7d258d905aa4 # v2.0.0
with:
- poetry-version: "2.1.1"
+ poetry-version: "2.2.1"
# Matches the `path` where we checkout Synapse above
working-directory: "synapse"
@@ -76,7 +76,7 @@ jobs:
run: |
set -x
DEBIAN_FRONTEND=noninteractive sudo apt-get install -yqq python3 pipx
- pipx install poetry==2.1.1
+ pipx install poetry==2.2.1
poetry remove -n twisted
poetry add -n --extras tls git+https://github.com/twisted/twisted.git#trunk
diff --git a/.github/workflows/fix_lint.yaml b/.github/workflows/fix_lint.yaml
index babc3bc5de..4752b6afeb 100644
--- a/.github/workflows/fix_lint.yaml
+++ b/.github/workflows/fix_lint.yaml
@@ -31,7 +31,7 @@ jobs:
uses: matrix-org/setup-python-poetry@5bbf6603c5c930615ec8a29f1b5d7d258d905aa4 # v2.0.0
with:
install-project: "false"
- poetry-version: "2.1.1"
+ poetry-version: "2.2.1"
- name: Run ruff check
continue-on-error: true
diff --git a/.github/workflows/latest_deps.yml b/.github/workflows/latest_deps.yml
index 1cc9863e4c..06d85237a8 100644
--- a/.github/workflows/latest_deps.yml
+++ b/.github/workflows/latest_deps.yml
@@ -54,7 +54,7 @@ jobs:
- uses: matrix-org/setup-python-poetry@5bbf6603c5c930615ec8a29f1b5d7d258d905aa4 # v2.0.0
with:
python-version: "3.x"
- poetry-version: "2.1.1"
+ poetry-version: "2.2.1"
extras: "all"
# Dump installed versions for debugging.
- run: poetry run pip list > before.txt
diff --git a/.github/workflows/push_complement_image.yml b/.github/workflows/push_complement_image.yml
index 12b4720ca5..12599457dc 100644
--- a/.github/workflows/push_complement_image.yml
+++ b/.github/workflows/push_complement_image.yml
@@ -47,6 +47,10 @@ jobs:
if: github.event_name == 'push'
with:
ref: master
+ # We use `poetry` in `complement.sh`
+ - uses: matrix-org/setup-python-poetry@5bbf6603c5c930615ec8a29f1b5d7d258d905aa4 # v2.0.0
+ with:
+ poetry-version: "2.2.1"
- name: Login to registry
uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3.7.0
with:
@@ -69,6 +73,7 @@ jobs:
run: |
for TAG in ${{ join(fromJson(steps.meta.outputs.json).tags, ' ') }}; do
echo "tag and push $TAG"
- docker tag complement-synapse $TAG
+ # `localhost/complement-synapse` should match the image created by `scripts-dev/complement.sh`
+ docker tag localhost/complement-synapse $TAG
docker push $TAG
done
diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml
index f75471c8a9..e85e7d336d 100644
--- a/.github/workflows/tests.yml
+++ b/.github/workflows/tests.yml
@@ -95,7 +95,7 @@ jobs:
- uses: matrix-org/setup-python-poetry@5bbf6603c5c930615ec8a29f1b5d7d258d905aa4 # v2.0.0
with:
python-version: "3.x"
- poetry-version: "2.1.1"
+ poetry-version: "2.2.1"
extras: "all"
- run: poetry run scripts-dev/generate_sample_config.sh --check
- run: poetry run scripts-dev/config-lint.sh
@@ -134,7 +134,7 @@ jobs:
- name: Setup Poetry
uses: matrix-org/setup-python-poetry@5bbf6603c5c930615ec8a29f1b5d7d258d905aa4 # v2.0.0
with:
- poetry-version: "2.1.1"
+ poetry-version: "2.2.1"
install-project: "false"
- name: Run ruff check
@@ -169,7 +169,7 @@ jobs:
# https://github.com/matrix-org/synapse/pull/15376#issuecomment-1498983775
# To make CI green, err towards caution and install the project.
install-project: "true"
- poetry-version: "2.1.1"
+ poetry-version: "2.2.1"
# Cribbed from
# https://github.com/AustinScola/mypy-cache-github-action/blob/85ea4f2972abed39b33bd02c36e341b28ca59213/src/restore.ts#L10-L17
@@ -265,7 +265,7 @@ jobs:
# Install like a normal project from source with all optional dependencies
extras: all
install-project: "true"
- poetry-version: "2.1.1"
+ poetry-version: "2.2.1"
- name: Ensure `Cargo.lock` is up to date (no stray changes after install)
# The `::error::` syntax is using GitHub Actions' error annotations, see
@@ -398,7 +398,7 @@ jobs:
- uses: matrix-org/setup-python-poetry@5bbf6603c5c930615ec8a29f1b5d7d258d905aa4 # v2.0.0
with:
python-version: ${{ matrix.job.python-version }}
- poetry-version: "2.1.1"
+ poetry-version: "2.2.1"
extras: ${{ matrix.job.extras }}
- name: Await PostgreSQL
if: ${{ matrix.job.postgres-version }}
@@ -500,7 +500,7 @@ jobs:
- uses: matrix-org/setup-python-poetry@5bbf6603c5c930615ec8a29f1b5d7d258d905aa4 # v2.0.0
with:
python-version: ${{ matrix.python-version }}
- poetry-version: "2.1.1"
+ poetry-version: "2.2.1"
extras: ${{ matrix.extras }}
- run: poetry run trial --jobs=2 tests
- name: Dump logs
@@ -595,7 +595,7 @@ jobs:
- run: sudo apt-get -qq install xmlsec1 postgresql-client
- uses: matrix-org/setup-python-poetry@5bbf6603c5c930615ec8a29f1b5d7d258d905aa4 # v2.0.0
with:
- poetry-version: "2.1.1"
+ poetry-version: "2.2.1"
extras: "postgres"
- run: .ci/scripts/test_export_data_command.sh
env:
@@ -648,7 +648,7 @@ jobs:
- uses: matrix-org/setup-python-poetry@5bbf6603c5c930615ec8a29f1b5d7d258d905aa4 # v2.0.0
with:
python-version: ${{ matrix.python-version }}
- poetry-version: "2.1.1"
+ poetry-version: "2.2.1"
extras: "postgres"
- run: .ci/scripts/test_synapse_port_db.sh
id: run_tester_script
diff --git a/.github/workflows/twisted_trunk.yml b/.github/workflows/twisted_trunk.yml
index c910e28f86..74e397a8bd 100644
--- a/.github/workflows/twisted_trunk.yml
+++ b/.github/workflows/twisted_trunk.yml
@@ -54,7 +54,7 @@ jobs:
with:
python-version: "3.x"
extras: "all"
- poetry-version: "2.1.1"
+ poetry-version: "2.2.1"
- run: |
poetry remove twisted
poetry add --extras tls git+https://github.com/twisted/twisted.git#${{ inputs.twisted_ref || 'trunk' }}
@@ -82,7 +82,7 @@ jobs:
with:
python-version: "3.x"
extras: "all test"
- poetry-version: "2.1.1"
+ poetry-version: "2.2.1"
- run: |
poetry remove twisted
poetry add --extras tls git+https://github.com/twisted/twisted.git#trunk
diff --git a/CHANGES.md b/CHANGES.md
index 1a5ff136c8..e7c2416238 100644
--- a/CHANGES.md
+++ b/CHANGES.md
@@ -1,3 +1,36 @@
+# Synapse 1.150.0rc1 (2026-03-17)
+
+## Features
+
+- Add experimental support for the [MSC4370](https://github.com/matrix-org/matrix-spec-proposals/pull/4370) Federation API `GET /extremities` endpoint. ([\#19314](https://github.com/element-hq/synapse/issues/19314))
+- [MSC4140: Cancellable delayed events](https://github.com/matrix-org/matrix-spec-proposals/pull/4140): When persisting a delayed event to the timeline, include its `delay_id` in the event's `unsigned` section in `/sync` responses to the event sender. ([\#19479](https://github.com/element-hq/synapse/issues/19479))
+- Expose [MSC4354 Sticky Events](https://github.com/matrix-org/matrix-spec-proposals/pull/4354) over the legacy (v3) /sync API. ([\#19487](https://github.com/element-hq/synapse/issues/19487))
+- When Matrix Authentication Service (MAS) integration is enabled, allow MAS to set the user locked status in Synapse. ([\#19554](https://github.com/element-hq/synapse/issues/19554))
+
+## Bugfixes
+
+- Fix `Build and push complement image` CI job pointing to non-existent image. ([\#19523](https://github.com/element-hq/synapse/issues/19523))
+- Fix a bug introduced in v1.26.0 that caused deactivated, erased users to not be removed from the user directory. ([\#19542](https://github.com/element-hq/synapse/issues/19542))
+
+## Improved Documentation
+
+- In the Admin API documentation, always express path parameters as `/` instead of as `/$param`. ([\#19307](https://github.com/element-hq/synapse/issues/19307))
+- Update docs to clarify `outbound_federation_restricted_to` can also be used with the [Secure Border Gateway (SBG)](https://element.io/en/server-suite/secure-border-gateways). ([\#19517](https://github.com/element-hq/synapse/issues/19517))
+- Unify Complement developer docs. ([\#19518](https://github.com/element-hq/synapse/issues/19518))
+
+## Internal Changes
+
+- Put membership updates in a background resumable task when changing the avatar or the display name. ([\#19311](https://github.com/element-hq/synapse/issues/19311))
+- Add in-repo Complement test to sanity check Synapse version matches git checkout (testing what we think we are). ([\#19476](https://github.com/element-hq/synapse/issues/19476))
+- Migrate `dev` dependencies to [PEP 735](https://peps.python.org/pep-0735/) dependency groups. ([\#19490](https://github.com/element-hq/synapse/issues/19490))
+- Remove the optional `systemd-python` dependency and the `systemd` extra on the `synapse` package. ([\#19491](https://github.com/element-hq/synapse/issues/19491))
+- Avoid re-computing the event ID when cloning events. ([\#19527](https://github.com/element-hq/synapse/issues/19527))
+- Allow caching of the `/versions` and `/auth_metadata` public endpoints. ([\#19530](https://github.com/element-hq/synapse/issues/19530))
+- Add a few labels to the number groupings in the `Processed request` logs. ([\#19548](https://github.com/element-hq/synapse/issues/19548))
+
+
+
+
# Synapse 1.149.1 (2026-03-11)
## Internal Changes
diff --git a/Cargo.lock b/Cargo.lock
index 340114f801..d6945bfbb7 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -13,9 +13,9 @@ dependencies = [
[[package]]
name = "anyhow"
-version = "1.0.101"
+version = "1.0.102"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5f0e0fee31ef5ed1ba1316088939cea399010ed7731dba877ed44aeb407a75ea"
+checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c"
[[package]]
name = "arc-swap"
@@ -844,9 +844,9 @@ dependencies = [
[[package]]
name = "pyo3-log"
-version = "0.13.2"
+version = "0.13.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "2f8bae9ad5ba08b0b0ed2bb9c2bdbaeccc69cafca96d78cf0fbcea0d45d122bb"
+checksum = "26c2ec80932c5c3b2d4fbc578c9b56b2d4502098587edb8bef5b6bfcad43682e"
dependencies = [
"arc-swap",
"log",
@@ -910,9 +910,9 @@ dependencies = [
[[package]]
name = "quinn-proto"
-version = "0.11.12"
+version = "0.11.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "49df843a9161c85bb8aae55f101bc0bac8bcafd637a620d9122fd7e0b2f7422e"
+checksum = "434b42fec591c96ef50e21e886936e66d3cc3f737104fdb9b737c40ffb94c098"
dependencies = [
"bytes",
"getrandom 0.3.3",
diff --git a/changelog.d/19311.misc b/changelog.d/19311.misc
deleted file mode 100644
index 66ec86c02d..0000000000
--- a/changelog.d/19311.misc
+++ /dev/null
@@ -1 +0,0 @@
-Put membership updates in a background resumable task when changing the avatar or the display name.
diff --git a/changelog.d/19314.feature b/changelog.d/19314.feature
deleted file mode 100644
index fd2893c577..0000000000
--- a/changelog.d/19314.feature
+++ /dev/null
@@ -1 +0,0 @@
-Add experimental support for the [MSC4370](https://github.com/matrix-org/matrix-spec-proposals/pull/4370) Federation API `GET /extremities` endpoint.
\ No newline at end of file
diff --git a/changelog.d/19430.removal b/changelog.d/19430.removal
new file mode 100644
index 0000000000..8920bccc38
--- /dev/null
+++ b/changelog.d/19430.removal
@@ -0,0 +1 @@
+Remove support for [MSC3852: Expose user agent information on Device](https://github.com/matrix-org/matrix-spec-proposals/pull/3852) as the MSC was closed.
\ No newline at end of file
diff --git a/changelog.d/19476.misc b/changelog.d/19476.misc
deleted file mode 100644
index c1869911a4..0000000000
--- a/changelog.d/19476.misc
+++ /dev/null
@@ -1 +0,0 @@
-Add in-repo Complement test to sanity check Synapse version matches git checkout (testing what we think we are).
diff --git a/changelog.d/19487.feature b/changelog.d/19487.feature
deleted file mode 100644
index 4eb1d8f261..0000000000
--- a/changelog.d/19487.feature
+++ /dev/null
@@ -1 +0,0 @@
-Expose [MSC4354 Sticky Events](https://github.com/matrix-org/matrix-spec-proposals/pull/4354) over the legacy (v3) /sync API.
\ No newline at end of file
diff --git a/changelog.d/19491.misc b/changelog.d/19491.misc
deleted file mode 100644
index 62b0ddddc2..0000000000
--- a/changelog.d/19491.misc
+++ /dev/null
@@ -1 +0,0 @@
-Remove the optional `systemd-python` dependency and the `systemd` extra on the `synapse` package.
diff --git a/changelog.d/19517.doc b/changelog.d/19517.doc
deleted file mode 100644
index 778c14c6aa..0000000000
--- a/changelog.d/19517.doc
+++ /dev/null
@@ -1 +0,0 @@
-Update docs to clarify `outbound_federation_restricted_to` can also be used with the [Secure Border Gateway (SBG)](https://element.io/en/server-suite/secure-border-gateways).
diff --git a/changelog.d/19518.doc b/changelog.d/19518.doc
deleted file mode 100644
index 5de867c8d7..0000000000
--- a/changelog.d/19518.doc
+++ /dev/null
@@ -1 +0,0 @@
-Unify Complement developer docs.
diff --git a/changelog.d/19542.bugfix b/changelog.d/19542.bugfix
deleted file mode 100644
index ab72504335..0000000000
--- a/changelog.d/19542.bugfix
+++ /dev/null
@@ -1 +0,0 @@
-Fix a bug introduced in v1.26.0 that caused deactivated, erased users to not be removed from the user directory.
\ No newline at end of file
diff --git a/changelog.d/19578.bugfix b/changelog.d/19578.bugfix
new file mode 100644
index 0000000000..dbaf4be7e8
--- /dev/null
+++ b/changelog.d/19578.bugfix
@@ -0,0 +1 @@
+Fix `Build and push complement image` CI job not having `poetry` available for the Complement runner script.
diff --git a/debian/build_virtualenv b/debian/build_virtualenv
index 70d4efcbd0..7bbf52ddd9 100755
--- a/debian/build_virtualenv
+++ b/debian/build_virtualenv
@@ -35,7 +35,7 @@ TEMP_VENV="$(mktemp -d)"
python3 -m venv "$TEMP_VENV"
source "$TEMP_VENV/bin/activate"
pip install -U pip
-pip install poetry==2.1.1 poetry-plugin-export==1.9.0
+pip install poetry==2.2.1 poetry-plugin-export==1.9.0
poetry export \
--extras all \
--extras test \
diff --git a/debian/changelog b/debian/changelog
index 9cd39ee76c..55af4c22b5 100644
--- a/debian/changelog
+++ b/debian/changelog
@@ -1,8 +1,13 @@
-matrix-synapse-py3 (1.149.1+nmu1) UNRELEASED; urgency=medium
+matrix-synapse-py3 (1.150.0~rc1) stable; urgency=medium
+ [ Quentin Gliech ]
* Change how the systemd journald integration is installed.
+ * Update Poetry used at build time to 2.2.1.
- -- Quentin Gliech Fri, 20 Feb 2026 19:19:51 +0100
+ [ Synapse Packaging team ]
+ * New synapse release 1.150.0rc1.
+
+ -- Synapse Packaging team Tue, 17 Mar 2026 14:56:35 +0000
matrix-synapse-py3 (1.149.1) stable; urgency=medium
diff --git a/docker/Dockerfile b/docker/Dockerfile
index 59771ae88f..6070d5c355 100644
--- a/docker/Dockerfile
+++ b/docker/Dockerfile
@@ -22,7 +22,7 @@
ARG DEBIAN_VERSION=trixie
ARG PYTHON_VERSION=3.13
-ARG POETRY_VERSION=2.1.1
+ARG POETRY_VERSION=2.2.1
###
### Stage 0: generate requirements.txt
diff --git a/docs/admin_api/user_admin_api.md b/docs/admin_api/user_admin_api.md
index 72e0e8d91a..14f86fa976 100644
--- a/docs/admin_api/user_admin_api.md
+++ b/docs/admin_api/user_admin_api.md
@@ -600,7 +600,7 @@ Fetches the number of invites sent by the provided user ID across all rooms
after the given timestamp.
```
-GET /_synapse/admin/v1/users/$user_id/sent_invite_count
+GET /_synapse/admin/v1/users//sent_invite_count
```
**Parameters**
@@ -634,7 +634,7 @@ Fetches the number of rooms that the user joined after the given timestamp, even
if they have subsequently left/been banned from those rooms.
```
-GET /_synapse/admin/v1/users/$/cumulative_joined_room_count
```
**Parameters**
@@ -1439,7 +1439,7 @@ The request and response format is the same as the
The API is:
```
-GET /_synapse/admin/v1/auth_providers/$provider/users/$external_id
+GET /_synapse/admin/v1/auth_providers//users/
```
When a user matched the given ID for the given provider, an HTTP code `200` with a response body like the following is returned:
@@ -1478,7 +1478,7 @@ _Added in Synapse 1.68.0._
The API is:
```
-GET /_synapse/admin/v1/threepid/$medium/users/$address
+GET /_synapse/admin/v1/threepid//users/
```
When a user matched the given address for the given medium, an HTTP code `200` with a response body like the following is returned:
@@ -1522,7 +1522,7 @@ is provided to override the default and allow the admin to issue the redactions
The API is
```
-POST /_synapse/admin/v1/user/$user_id/redact
+POST /_synapse/admin/v1/user//redact
{
"rooms": ["!roomid1", "!roomid2"]
@@ -1571,7 +1571,7 @@ or until Synapse is restarted (whichever happens first).
The API is:
```
-GET /_synapse/admin/v1/user/redact_status/$redact_id
+GET /_synapse/admin/v1/user/redact_status/
```
A response body like the following is returned:
diff --git a/docs/development/dependencies.md b/docs/development/dependencies.md
index 1b3348703f..fe0667194a 100644
--- a/docs/development/dependencies.md
+++ b/docs/development/dependencies.md
@@ -6,7 +6,7 @@ This is a quick cheat sheet for developers on how to use [`poetry`](https://pyth
See the [contributing guide](contributing_guide.md#4-install-the-dependencies).
-Developers should use Poetry 1.3.2 or higher. If you encounter problems related
+Developers should use Poetry 2.2.0 or higher. If you encounter problems related
to poetry, please [double-check your poetry version](#check-the-version-of-poetry-with-poetry---version).
# Background
diff --git a/docs/upgrade.md b/docs/upgrade.md
index 777e57c492..aeae82c114 100644
--- a/docs/upgrade.md
+++ b/docs/upgrade.md
@@ -146,6 +146,14 @@ No immediate change is necessary, however once the parameter is removed, modules
From this version, when the parameter is passed, an error such as
``Deprecated `deactivation` parameter passed to `set_displayname` Module API (value: False). This will break in 2027.`` will be logged. The method will otherwise continue to work.
+## Updated request log format (`Processed request: ...`)
+
+The [request log format](usage/administration/request_log.md) has slightly changed to
+include `ru=(...)` and `db=(...)` labels to better disambiguate the number groupings.
+Previously, these values appeared without labels.
+
+This only matters if you have third-party tooling that parses the Synapse logs.
+
# Upgrading to v1.146.0
## Drop support for Ubuntu 25.04 Plucky Puffin, and add support for 25.10 Questing Quokka
diff --git a/docs/usage/administration/request_log.md b/docs/usage/administration/request_log.md
index 6154108934..7e3047eb62 100644
--- a/docs/usage/administration/request_log.md
+++ b/docs/usage/administration/request_log.md
@@ -5,8 +5,8 @@ HTTP request logs are written by synapse (see [`synapse/http/site.py`](https://g
See the following for how to decode the dense data available from the default logging configuration.
```
-2020-10-01 12:00:00,000 - synapse.access.http.8008 - 311 - INFO - PUT-1000- 192.168.0.1 - 8008 - {another-matrix-server.com} Processed request: 0.100sec/-0.000sec (0.000sec, 0.000sec) (0.001sec/0.090sec/3) 11B !200 "PUT /_matrix/federation/v1/send/1600000000000 HTTP/1.1" "Synapse/1.20.1" [0 dbevts]
--AAAAAAAAAAAAAAAAAAAAA- -BBBBBBBBBBBBBBBBBBBBBB- -C- -DD- -EEEEEE- -FFFFFFFFF- -GG- -HHHHHHHHHHHHHHHHHHHHHHH- -IIIIII- -JJJJJJJ- -KKKKKK-, -LLLLLL- -MMMMMMM- -NNNNNN- O -P- -QQ- -RRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRR- -SSSSSSSSSSSS- -TTTTTT-
+2020-10-01 12:00:00,000 - synapse.access.http.8008 - 311 - INFO - PUT-1000- 192.168.0.1 - 8008 - {another-matrix-server.com} Processed request: 0.100sec/-0.000sec ru=(0.000sec, 0.000sec) db=(0.001sec/0.090sec/3) 11B !200 "PUT /_matrix/federation/v1/send/1600000000000 HTTP/1.1" "Synapse/1.20.1" [0 dbevts]
+-AAAAAAAAAAAAAAAAAAAAA- -BBBBBBBBBBBBBBBBBBBBBB- -C- -DD- -EEEEEE- -FFFFFFFFF- -GG- -HHHHHHHHHHHHHHHHHHHHHHH- -IIIIII- -JJJJJJJ- -KKKKKK-, -LLLLLL- -MMMMMM- -NNNNNN- O -P- -QQ- -RRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRR- -SSSSSSSSSSSS- -TTTTTT-
```
diff --git a/mypy.ini b/mypy.ini
index d6a3434293..ec73ce9f6e 100644
--- a/mypy.ini
+++ b/mypy.ini
@@ -69,7 +69,7 @@ warn_unused_ignores = False
;; https://github.com/python/typeshed/tree/master/stubs
;; and for each package `foo` there's a corresponding `types-foo` package on PyPI,
;; which we can pull in as a dev dependency by adding to `pyproject.toml`'s
-;; `[tool.poetry.group.dev.dependencies]` list.
+;; `[dependency-groups]` `dev` list.
# https://github.com/lepture/authlib/issues/460
[mypy-authlib.*]
diff --git a/poetry.lock b/poetry.lock
index 0d03c860f5..681a183b42 100644
--- a/poetry.lock
+++ b/poetry.lock
@@ -31,7 +31,7 @@ description = "The ultimate Python library in building OAuth and OpenID Connect
optional = true
python-versions = ">=3.9"
groups = ["main"]
-markers = "extra == \"oidc\" or extra == \"jwt\" or extra == \"all\""
+markers = "extra == \"all\" or extra == \"jwt\" or extra == \"oidc\""
files = [
{file = "authlib-1.6.9-py2.py3-none-any.whl", hash = "sha256:f08b4c14e08f0861dc18a32357b33fbcfd2ea86cfe3fe149484b4d764c4a0ac3"},
{file = "authlib-1.6.9.tar.gz", hash = "sha256:d8f2421e7e5980cc1ddb4e32d3f5fa659cfaf60d8eaf3281ebed192e4ab74f04"},
@@ -531,7 +531,7 @@ description = "XML bomb protection for Python stdlib modules"
optional = true
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
groups = ["main"]
-markers = "extra == \"saml2\" or extra == \"all\""
+markers = "extra == \"all\" or extra == \"saml2\""
files = [
{file = "defusedxml-0.7.1-py2.py3-none-any.whl", hash = "sha256:a352e7e428770286cc899e2542b6cdaedb2b4953ff269a210103ec58f6198a61"},
{file = "defusedxml-0.7.1.tar.gz", hash = "sha256:1bb3032db185915b62d7c6209c5a8792be6a32ab2fedacc84e01b52c51aa3e69"},
@@ -556,7 +556,7 @@ description = "XPath 1.0/2.0/3.0/3.1 parsers and selectors for ElementTree and l
optional = true
python-versions = ">=3.8"
groups = ["main"]
-markers = "extra == \"saml2\" or extra == \"all\""
+markers = "extra == \"all\" or extra == \"saml2\""
files = [
{file = "elementpath-4.8.0-py3-none-any.whl", hash = "sha256:5393191f84969bcf8033b05ec4593ef940e58622ea13cefe60ecefbbf09d58d9"},
{file = "elementpath-4.8.0.tar.gz", hash = "sha256:5822a2560d99e2633d95f78694c7ff9646adaa187db520da200a8e9479dc46ae"},
@@ -606,7 +606,7 @@ description = "Python wrapper for hiredis"
optional = true
python-versions = ">=3.8"
groups = ["main"]
-markers = "extra == \"redis\" or extra == \"all\""
+markers = "extra == \"all\" or extra == \"redis\""
files = [
{file = "hiredis-3.3.0-cp310-cp310-macosx_10_15_universal2.whl", hash = "sha256:9937d9b69321b393fbace69f55423480f098120bc55a3316e1ca3508c4dbbd6f"},
{file = "hiredis-3.3.0-cp310-cp310-macosx_10_15_x86_64.whl", hash = "sha256:50351b77f89ba6a22aff430b993653847f36b71d444509036baa0f2d79d1ebf4"},
@@ -930,7 +930,7 @@ description = "Jaeger Python OpenTracing Tracer implementation"
optional = true
python-versions = ">=3.7"
groups = ["main"]
-markers = "extra == \"opentracing\" or extra == \"all\""
+markers = "extra == \"all\" or extra == \"opentracing\""
files = [
{file = "jaeger-client-4.8.0.tar.gz", hash = "sha256:3157836edab8e2c209bd2d6ae61113db36f7ee399e66b1dcbb715d87ab49bfe0"},
]
@@ -1122,7 +1122,7 @@ description = "A strictly RFC 4510 conforming LDAP V3 pure Python client library
optional = true
python-versions = "*"
groups = ["main"]
-markers = "extra == \"matrix-synapse-ldap3\" or extra == \"all\""
+markers = "extra == \"all\" or extra == \"matrix-synapse-ldap3\""
files = [
{file = "ldap3-2.9.1-py2.py3-none-any.whl", hash = "sha256:5869596fc4948797020d3f03b7939da938778a0f9e2009f7a072ccf92b8e8d70"},
{file = "ldap3-2.9.1.tar.gz", hash = "sha256:f3e7fc4718e3f09dda568b57100095e0ce58633bcabbed8667ce3f8fbaa4229f"},
@@ -1239,7 +1239,7 @@ description = "Powerful and Pythonic XML processing library combining libxml2/li
optional = true
python-versions = ">=3.8"
groups = ["main"]
-markers = "extra == \"url-preview\" or extra == \"all\""
+markers = "extra == \"all\" or extra == \"url-preview\""
files = [
{file = "lxml-6.0.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:e77dd455b9a16bbd2a5036a63ddbd479c19572af81b624e79ef422f929eef388"},
{file = "lxml-6.0.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:5d444858b9f07cefff6455b983aea9a67f7462ba1f6cbe4a21e8bf6791bf2153"},
@@ -1553,7 +1553,7 @@ description = "An LDAP3 auth provider for Synapse"
optional = true
python-versions = ">=3.10"
groups = ["main"]
-markers = "extra == \"matrix-synapse-ldap3\" or extra == \"all\""
+markers = "extra == \"all\" or extra == \"matrix-synapse-ldap3\""
files = [
{file = "matrix_synapse_ldap3-0.4.0-py3-none-any.whl", hash = "sha256:bf080037230d2af5fd3639cb87266de65c1cad7a68ea206278c5b4bf9c1a17f3"},
{file = "matrix_synapse_ldap3-0.4.0.tar.gz", hash = "sha256:cff52ba780170de5e6e8af42863d2648ee23f3bf0a9fea6db52372f9fc00be2b"},
@@ -1834,7 +1834,7 @@ description = "OpenTracing API for Python. See documentation at http://opentraci
optional = true
python-versions = "*"
groups = ["main"]
-markers = "extra == \"opentracing\" or extra == \"all\""
+markers = "extra == \"all\" or extra == \"opentracing\""
files = [
{file = "opentracing-2.4.0.tar.gz", hash = "sha256:a173117e6ef580d55874734d1fa7ecb6f3655160b8b8974a2a1e98e5ec9c840d"},
]
@@ -2032,7 +2032,7 @@ description = "psycopg2 - Python-PostgreSQL Database Adapter"
optional = true
python-versions = ">=3.9"
groups = ["main"]
-markers = "extra == \"postgres\" or extra == \"all\""
+markers = "extra == \"all\" or extra == \"postgres\""
files = [
{file = "psycopg2-2.9.11-cp310-cp310-win_amd64.whl", hash = "sha256:103e857f46bb76908768ead4e2d0ba1d1a130e7b8ed77d3ae91e8b33481813e8"},
{file = "psycopg2-2.9.11-cp311-cp311-win_amd64.whl", hash = "sha256:210daed32e18f35e3140a1ebe059ac29209dd96468f2f7559aa59f75ee82a5cb"},
@@ -2050,7 +2050,7 @@ description = ".. image:: https://travis-ci.org/chtd/psycopg2cffi.svg?branch=mas
optional = true
python-versions = "*"
groups = ["main"]
-markers = "platform_python_implementation == \"PyPy\" and (extra == \"postgres\" or extra == \"all\")"
+markers = "platform_python_implementation == \"PyPy\" and (extra == \"all\" or extra == \"postgres\")"
files = [
{file = "psycopg2cffi-2.9.0.tar.gz", hash = "sha256:7e272edcd837de3a1d12b62185eb85c45a19feda9e62fa1b120c54f9e8d35c52"},
]
@@ -2066,7 +2066,7 @@ description = "A Simple library to enable psycopg2 compatability"
optional = true
python-versions = "*"
groups = ["main"]
-markers = "platform_python_implementation == \"PyPy\" and (extra == \"postgres\" or extra == \"all\")"
+markers = "platform_python_implementation == \"PyPy\" and (extra == \"all\" or extra == \"postgres\")"
files = [
{file = "psycopg2cffi-compat-1.1.tar.gz", hash = "sha256:d25e921748475522b33d13420aad5c2831c743227dc1f1f2585e0fdb5c914e05"},
]
@@ -2306,14 +2306,14 @@ windows-terminal = ["colorama (>=0.4.6)"]
[[package]]
name = "pyjwt"
-version = "2.11.0"
+version = "2.12.0"
description = "JSON Web Token implementation in Python"
optional = false
python-versions = ">=3.9"
groups = ["dev"]
files = [
- {file = "pyjwt-2.11.0-py3-none-any.whl", hash = "sha256:94a6bde30eb5c8e04fee991062b534071fd1439ef58d2adc9ccb823e7bcd0469"},
- {file = "pyjwt-2.11.0.tar.gz", hash = "sha256:35f95c1f0fbe5d5ba6e43f00271c275f7a1a4db1dab27bf708073b75318ea623"},
+ {file = "pyjwt-2.12.0-py3-none-any.whl", hash = "sha256:9bb459d1bdd0387967d287f5656bf7ec2b9a26645d1961628cda1764e087fd6e"},
+ {file = "pyjwt-2.12.0.tar.gz", hash = "sha256:2f62390b667cd8257de560b850bb5a883102a388829274147f1d724453f8fb02"},
]
[package.dependencies]
@@ -2348,7 +2348,7 @@ description = "A development tool to measure, monitor and analyze the memory beh
optional = true
python-versions = ">=3.6"
groups = ["main"]
-markers = "extra == \"cache-memory\" or extra == \"all\""
+markers = "extra == \"all\" or extra == \"cache-memory\""
files = [
{file = "Pympler-1.0.1-py3-none-any.whl", hash = "sha256:d260dda9ae781e1eab6ea15bacb84015849833ba5555f141d2d9b7b7473b307d"},
{file = "Pympler-1.0.1.tar.gz", hash = "sha256:993f1a3599ca3f4fcd7160c7545ad06310c9e12f70174ae7ae8d4e25f6c5d3fa"},
@@ -2398,18 +2398,18 @@ tests = ["hypothesis (>=3.27.0)", "pytest (>=7.4.0)", "pytest-cov (>=2.10.1)", "
[[package]]
name = "pyopenssl"
-version = "25.3.0"
+version = "26.0.0"
description = "Python wrapper module around the OpenSSL library"
optional = false
-python-versions = ">=3.7"
+python-versions = ">=3.8"
groups = ["main"]
files = [
- {file = "pyopenssl-25.3.0-py3-none-any.whl", hash = "sha256:1fda6fc034d5e3d179d39e59c1895c9faeaf40a79de5fc4cbbfbe0d36f4a77b6"},
- {file = "pyopenssl-25.3.0.tar.gz", hash = "sha256:c981cb0a3fd84e8602d7afc209522773b94c1c2446a3c710a75b06fe1beae329"},
+ {file = "pyopenssl-26.0.0-py3-none-any.whl", hash = "sha256:df94d28498848b98cc1c0ffb8ef1e71e40210d3b0a8064c9d29571ed2904bf81"},
+ {file = "pyopenssl-26.0.0.tar.gz", hash = "sha256:f293934e52936f2e3413b89c6ce36df66a0b34ae1ea3a053b8c5020ff2f513fc"},
]
[package.dependencies]
-cryptography = ">=45.0.7,<47"
+cryptography = ">=46.0.0,<47"
typing-extensions = {version = ">=4.9", markers = "python_version < \"3.13\" and python_version >= \"3.8\""}
[package.extras]
@@ -2480,7 +2480,7 @@ description = "Python implementation of SAML Version 2 Standard"
optional = true
python-versions = ">=3.9,<4.0"
groups = ["main"]
-markers = "extra == \"saml2\" or extra == \"all\""
+markers = "extra == \"all\" or extra == \"saml2\""
files = [
{file = "pysaml2-7.5.0-py3-none-any.whl", hash = "sha256:bc6627cc344476a83c757f440a73fda1369f13b6fda1b4e16bca63ffbabb5318"},
{file = "pysaml2-7.5.0.tar.gz", hash = "sha256:f36871d4e5ee857c6b85532e942550d2cf90ea4ee943d75eb681044bbc4f54f7"},
@@ -2505,7 +2505,7 @@ description = "Extensions to the standard Python datetime module"
optional = true
python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7"
groups = ["main"]
-markers = "extra == \"saml2\" or extra == \"all\""
+markers = "extra == \"all\" or extra == \"saml2\""
files = [
{file = "python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3"},
{file = "python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427"},
@@ -2533,7 +2533,7 @@ description = "World timezone definitions, modern and historical"
optional = true
python-versions = "*"
groups = ["main"]
-markers = "extra == \"saml2\" or extra == \"all\""
+markers = "extra == \"all\" or extra == \"saml2\""
files = [
{file = "pytz-2026.1.post1-py2.py3-none-any.whl", hash = "sha256:f2fd16142fda348286a75e1a524be810bb05d444e5a081f37f7affc635035f7a"},
{file = "pytz-2026.1.post1.tar.gz", hash = "sha256:3378dde6a0c3d26719182142c56e60c7f9af7e968076f31aae569d72a0358ee1"},
@@ -2937,7 +2937,7 @@ description = "Python client for Sentry (https://sentry.io)"
optional = true
python-versions = ">=3.6"
groups = ["main"]
-markers = "extra == \"sentry\" or extra == \"all\""
+markers = "extra == \"all\" or extra == \"sentry\""
files = [
{file = "sentry_sdk-2.54.0-py2.py3-none-any.whl", hash = "sha256:fd74e0e281dcda63afff095d23ebcd6e97006102cdc8e78a29f19ecdf796a0de"},
{file = "sentry_sdk-2.54.0.tar.gz", hash = "sha256:2620c2575128d009b11b20f7feb81e4e4e8ae08ec1d36cbc845705060b45cc1b"},
@@ -3136,7 +3136,7 @@ description = "Tornado IOLoop Backed Concurrent Futures"
optional = true
python-versions = "*"
groups = ["main"]
-markers = "extra == \"opentracing\" or extra == \"all\""
+markers = "extra == \"all\" or extra == \"opentracing\""
files = [
{file = "threadloop-1.0.2-py2-none-any.whl", hash = "sha256:5c90dbefab6ffbdba26afb4829d2a9df8275d13ac7dc58dccb0e279992679599"},
{file = "threadloop-1.0.2.tar.gz", hash = "sha256:8b180aac31013de13c2ad5c834819771992d350267bddb854613ae77ef571944"},
@@ -3152,7 +3152,7 @@ description = "Python bindings for the Apache Thrift RPC system"
optional = true
python-versions = "*"
groups = ["main"]
-markers = "extra == \"opentracing\" or extra == \"all\""
+markers = "extra == \"all\" or extra == \"opentracing\""
files = [
{file = "thrift-0.22.0.tar.gz", hash = "sha256:42e8276afbd5f54fe1d364858b6877bc5e5a4a5ed69f6a005b94ca4918fe1466"},
]
@@ -3222,25 +3222,23 @@ markers = {main = "python_version < \"3.14\""}
[[package]]
name = "tornado"
-version = "6.5.4"
+version = "6.5.5"
description = "Tornado is a Python web framework and asynchronous networking library, originally developed at FriendFeed."
optional = true
python-versions = ">=3.9"
groups = ["main"]
-markers = "extra == \"opentracing\" or extra == \"all\""
+markers = "extra == \"all\" or extra == \"opentracing\""
files = [
- {file = "tornado-6.5.4-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:d6241c1a16b1c9e4cc28148b1cda97dd1c6cb4fb7068ac1bedc610768dff0ba9"},
- {file = "tornado-6.5.4-cp39-abi3-macosx_10_9_x86_64.whl", hash = "sha256:2d50f63dda1d2cac3ae1fa23d254e16b5e38153758470e9956cbc3d813d40843"},
- {file = "tornado-6.5.4-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d1cf66105dc6acb5af613c054955b8137e34a03698aa53272dbda4afe252be17"},
- {file = "tornado-6.5.4-cp39-abi3-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:50ff0a58b0dc97939d29da29cd624da010e7f804746621c78d14b80238669335"},
- {file = "tornado-6.5.4-cp39-abi3-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e5fb5e04efa54cf0baabdd10061eb4148e0be137166146fff835745f59ab9f7f"},
- {file = "tornado-6.5.4-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:9c86b1643b33a4cd415f8d0fe53045f913bf07b4a3ef646b735a6a86047dda84"},
- {file = "tornado-6.5.4-cp39-abi3-musllinux_1_2_i686.whl", hash = "sha256:6eb82872335a53dd063a4f10917b3efd28270b56a33db69009606a0312660a6f"},
- {file = "tornado-6.5.4-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:6076d5dda368c9328ff41ab5d9dd3608e695e8225d1cd0fd1e006f05da3635a8"},
- {file = "tornado-6.5.4-cp39-abi3-win32.whl", hash = "sha256:1768110f2411d5cd281bac0a090f707223ce77fd110424361092859e089b38d1"},
- {file = "tornado-6.5.4-cp39-abi3-win_amd64.whl", hash = "sha256:fa07d31e0cd85c60713f2b995da613588aa03e1303d75705dca6af8babc18ddc"},
- {file = "tornado-6.5.4-cp39-abi3-win_arm64.whl", hash = "sha256:053e6e16701eb6cbe641f308f4c1a9541f91b6261991160391bfc342e8a551a1"},
- {file = "tornado-6.5.4.tar.gz", hash = "sha256:a22fa9047405d03260b483980635f0b041989d8bcc9a313f8fe18b411d84b1d7"},
+ {file = "tornado-6.5.5-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:487dc9cc380e29f58c7ab88f9e27cdeef04b2140862e5076a66fb6bb68bb1bfa"},
+ {file = "tornado-6.5.5-cp39-abi3-macosx_10_9_x86_64.whl", hash = "sha256:65a7f1d46d4bb41df1ac99f5fcb685fb25c7e61613742d5108b010975a9a6521"},
+ {file = "tornado-6.5.5-cp39-abi3-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:e74c92e8e65086b338fd56333fb9a68b9f6f2fe7ad532645a290a464bcf46be5"},
+ {file = "tornado-6.5.5-cp39-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:435319e9e340276428bbdb4e7fa732c2d399386d1de5686cb331ec8eee754f07"},
+ {file = "tornado-6.5.5-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:3f54aa540bdbfee7b9eb268ead60e7d199de5021facd276819c193c0fb28ea4e"},
+ {file = "tornado-6.5.5-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:36abed1754faeb80fbd6e64db2758091e1320f6bba74a4cf8c09cd18ccce8aca"},
+ {file = "tornado-6.5.5-cp39-abi3-win32.whl", hash = "sha256:dd3eafaaeec1c7f2f8fdcd5f964e8907ad788fe8a5a32c4426fbbdda621223b7"},
+ {file = "tornado-6.5.5-cp39-abi3-win_amd64.whl", hash = "sha256:6443a794ba961a9f619b1ae926a2e900ac20c34483eea67be4ed8f1e58d3ef7b"},
+ {file = "tornado-6.5.5-cp39-abi3-win_arm64.whl", hash = "sha256:2c9a876e094109333f888539ddb2de4361743e5d21eece20688e3e351e4990a6"},
+ {file = "tornado-6.5.5.tar.gz", hash = "sha256:192b8f3ea91bd7f1f50c06955416ed76c6b72f96779b962f07f911b91e8d30e9"},
]
[[package]]
@@ -3361,7 +3359,7 @@ description = "non-blocking redis client for python"
optional = true
python-versions = "*"
groups = ["main"]
-markers = "extra == \"redis\" or extra == \"all\""
+markers = "extra == \"all\" or extra == \"redis\""
files = [
{file = "txredisapi-1.4.11-py3-none-any.whl", hash = "sha256:ac64d7a9342b58edca13ef267d4fa7637c1aa63f8595e066801c1e8b56b22d0b"},
{file = "txredisapi-1.4.11.tar.gz", hash = "sha256:3eb1af99aefdefb59eb877b1dd08861efad60915e30ad5bf3d5bf6c5cedcdbc6"},
@@ -3622,7 +3620,7 @@ description = "An XML Schema validator and decoder"
optional = true
python-versions = ">=3.7"
groups = ["main"]
-markers = "extra == \"saml2\" or extra == \"all\""
+markers = "extra == \"all\" or extra == \"saml2\""
files = [
{file = "xmlschema-2.5.1-py3-none-any.whl", hash = "sha256:ec2b2a15c8896c1fcd14dcee34ca30032b99456c3c43ce793fdb9dca2fb4b869"},
{file = "xmlschema-2.5.1.tar.gz", hash = "sha256:4f7497de6c8b6dc2c28ad7b9ed6e21d186f4afe248a5bea4f54eedab4da44083"},
@@ -3756,4 +3754,4 @@ url-preview = ["lxml"]
[metadata]
lock-version = "2.1"
python-versions = ">=3.10.0,<4.0.0"
-content-hash = "dd63614889e7e181fca33760741a490e65fe4ef4f42756cafd0f804ae7324916"
+content-hash = "ce9ac9da9e7ffaf24b3e1e7892342ba486e7af4ea25385f875d0f3a2d5c5d133"
diff --git a/pyproject.toml b/pyproject.toml
index 07ab1ee5db..adb9993aae 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -1,6 +1,6 @@
[project]
name = "matrix-synapse"
-version = "1.149.1"
+version = "1.150.0rc1"
description = "Homeserver for the Matrix decentralised comms protocol"
readme = "README.rst"
authors = [
@@ -224,6 +224,9 @@ update_synapse_database = "synapse._scripts.update_synapse_database:main"
[tool.poetry]
packages = [{ include = "synapse" }]
+# We're using PEP 735 dependency groups, which requires Poetry 2.2.0+
+requires-poetry = ">=2.2.0"
+
[tool.poetry.build]
# Compile our rust module when using `poetry install`. This is still required
# while using `poetry` as the build frontend. Saves the developer from needing
@@ -251,55 +254,55 @@ generate-setup-file = true
# Dependencies used for developing Synapse itself.
#
-# Hold off on migrating these to `dev-dependencies` (PEP 735) for now until
-# Poetry 2.2.0+, pip 25.1+ are more widely available.
-[tool.poetry.group.dev.dependencies]
# We pin development dependencies in poetry.lock so that our tests don't start
# failing on new releases. Keeping lower bounds loose here means that dependabot
# can bump versions without having to update the content-hash in the lockfile.
# This helps prevents merge conflicts when running a batch of dependabot updates.
-ruff = "0.14.6"
+[dependency-groups]
+dev = [
+ "ruff==0.14.6",
-# Typechecking
-lxml-stubs = ">=0.4.0"
-mypy = "*"
-mypy-zope = "*"
-types-bleach = ">=4.1.0"
-types-jsonschema = ">=3.2.0"
-types-netaddr = ">=0.8.0.6"
-types-opentracing = ">=2.4.2"
-types-Pillow = ">=8.3.4"
-types-psycopg2 = ">=2.9.9"
-types-pyOpenSSL = ">=20.0.7"
-types-PyYAML = ">=5.4.10"
-types-requests = ">=2.26.0"
-types-setuptools = ">=57.4.0"
+ # Typechecking
+ "lxml-stubs>=0.4.0",
+ "mypy",
+ "mypy-zope",
+ "types-bleach>=4.1.0",
+ "types-jsonschema>=3.2.0",
+ "types-netaddr>=0.8.0.6",
+ "types-opentracing>=2.4.2",
+ "types-Pillow>=8.3.4",
+ "types-psycopg2>=2.9.9",
+ "types-pyOpenSSL>=20.0.7",
+ "types-PyYAML>=5.4.10",
+ "types-requests>=2.26.0",
+ "types-setuptools>=57.4.0",
-# Dependencies which are exclusively required by unit test code. This is
-# NOT a list of all modules that are necessary to run the unit tests.
-# Tests assume that all optional dependencies are installed.
-#
-# If this is updated, don't forget to update the equivalent lines in
-# project.optional-dependencies.test.
-parameterized = ">=0.9.0"
-idna = ">=3.3"
+ # Dependencies which are exclusively required by unit test code. This is
+ # NOT a list of all modules that are necessary to run the unit tests.
+ # Tests assume that all optional dependencies are installed.
+ #
+ # If this is updated, don't forget to update the equivalent lines in
+ # project.optional-dependencies.test.
+ "parameterized>=0.9.0",
+ "idna>=3.3",
-# The following are used by the release script
-click = ">=8.1.3"
-# GitPython was == 3.1.14; bumped to 3.1.20, the first release with type hints.
-GitPython = ">=3.1.20"
-markdown-it-py = ">=3.0.0"
-pygithub = ">=1.59"
-# The following are executed as commands by the release script.
-twine = "*"
-# Towncrier min version comes from https://github.com/matrix-org/synapse/pull/3425. Rationale unclear.
-towncrier = ">=18.6.0rc1"
+ # The following are used by the release script
+ "click>=8.1.3",
+ # GitPython was == 3.1.14; bumped to 3.1.20, the first release with type hints.
+ "GitPython>=3.1.20",
+ "markdown-it-py>=3.0.0",
+ "pygithub>=1.59",
+ # The following are executed as commands by the release script.
+ "twine",
+ # Towncrier min version comes from https://github.com/matrix-org/synapse/pull/3425. Rationale unclear.
+ "towncrier>=18.6.0rc1",
-# Used for checking the Poetry lockfile
-tomli = ">=1.2.3"
+ # Used for checking the Poetry lockfile
+ "tomli>=1.2.3",
-# Used for checking the schema delta files
-sqlglot = ">=28.0.0"
+ # Used for checking the schema delta files
+ "sqlglot>=28.0.0",
+]
[tool.towncrier]
package = "synapse"
diff --git a/rust/src/events/internal_metadata.rs b/rust/src/events/internal_metadata.rs
index fa40fdcfad..595f9cf7eb 100644
--- a/rust/src/events/internal_metadata.rs
+++ b/rust/src/events/internal_metadata.rs
@@ -57,6 +57,7 @@ enum EventInternalMetadataData {
PolicyServerSpammy(bool),
Redacted(bool),
TxnId(Box),
+ DelayId(Box),
TokenId(i64),
DeviceId(Box),
}
@@ -115,6 +116,10 @@ impl EventInternalMetadataData {
pyo3::intern!(py, "txn_id"),
o.into_pyobject(py).unwrap_infallible().into_any(),
),
+ EventInternalMetadataData::DelayId(o) => (
+ pyo3::intern!(py, "delay_id"),
+ o.into_pyobject(py).unwrap_infallible().into_any(),
+ ),
EventInternalMetadataData::TokenId(o) => (
pyo3::intern!(py, "token_id"),
o.into_pyobject(py).unwrap_infallible().into_any(),
@@ -179,6 +184,12 @@ impl EventInternalMetadataData {
.map(String::into_boxed_str)
.with_context(|| format!("'{key_str}' has invalid type"))?,
),
+ "delay_id" => EventInternalMetadataData::DelayId(
+ value
+ .extract()
+ .map(String::into_boxed_str)
+ .with_context(|| format!("'{key_str}' has invalid type"))?,
+ ),
"token_id" => EventInternalMetadataData::TokenId(
value
.extract()
@@ -472,6 +483,17 @@ impl EventInternalMetadata {
set_property!(self, TxnId, obj.into_boxed_str());
}
+ /// The delay ID, set only if the event was a delayed event.
+ #[getter]
+ fn get_delay_id(&self) -> PyResult<&str> {
+ let s = get_property!(self, DelayId)?;
+ Ok(s)
+ }
+ #[setter]
+ fn set_delay_id(&mut self, obj: String) {
+ set_property!(self, DelayId, obj.into_boxed_str());
+ }
+
/// The access token ID of the user who sent this event, if any.
#[getter]
fn get_token_id(&self) -> PyResult {
diff --git a/schema/synapse-config.schema.yaml b/schema/synapse-config.schema.yaml
index 3a61a4c6fc..dbf7d7acb7 100644
--- a/schema/synapse-config.schema.yaml
+++ b/schema/synapse-config.schema.yaml
@@ -1,5 +1,5 @@
$schema: https://element-hq.github.io/synapse/latest/schema/v1/meta.schema.json
-$id: https://element-hq.github.io/synapse/schema/synapse/v1.149/synapse-config.schema.json
+$id: https://element-hq.github.io/synapse/schema/synapse/v1.150/synapse-config.schema.json
type: object
properties:
modules:
diff --git a/scripts-dev/complement.sh b/scripts-dev/complement.sh
index c65ae53df0..a8a361fd4a 100755
--- a/scripts-dev/complement.sh
+++ b/scripts-dev/complement.sh
@@ -38,17 +38,22 @@ set -e
# Tag local builds with a dummy registry namespace so that later builds may reference
# them exactly instead of accidentally pulling from a remote registry.
#
-# This is important as some storage drivers/types prefer remote images over local
-# (`containerd`) which causes problems as we're testing against some remote image that
-# doesn't include all of the changes that we're trying to test (be it locally or in a PR
-# in CI). This is spawning from a real-world problem where the GitHub runners were
+# This is important as some Docker storage drivers/types prefer remote images over local
+# (like `containerd`) which causes problems as we're testing against some remote image
+# that doesn't include all of the changes that we're trying to test (be it locally or in
+# a PR in CI). This is spawning from a real-world problem where the GitHub runners were
# updated to use Docker Engine 29.0.0+ which uses `containerd` by default for new
# installations.
+#
+# XXX: If the Docker image name changes, don't forget to update
+# `.github/workflows/push_complement_image.yml` as well
LOCAL_IMAGE_NAMESPACE=localhost
# The image tags for how these images will be stored in the registry
SYNAPSE_IMAGE_PATH="$LOCAL_IMAGE_NAMESPACE/synapse"
SYNAPSE_WORKERS_IMAGE_PATH="$LOCAL_IMAGE_NAMESPACE/synapse-workers"
+# XXX: If the Docker image name changes, don't forget to update
+# `.github/workflows/push_complement_image.yml` as well
COMPLEMENT_SYNAPSE_IMAGE_PATH="$LOCAL_IMAGE_NAMESPACE/complement-synapse"
SYNAPSE_EDITABLE_IMAGE_PATH="$LOCAL_IMAGE_NAMESPACE/synapse-editable"
diff --git a/synapse/config/experimental.py b/synapse/config/experimental.py
index a8c9305704..41efff1d8f 100644
--- a/synapse/config/experimental.py
+++ b/synapse/config/experimental.py
@@ -445,9 +445,6 @@ class ExperimentalConfig(Config):
# MSC3848: Introduce errcodes for specific event sending failures
self.msc3848_enabled: bool = experimental.get("msc3848_enabled", False)
- # MSC3852: Expose last seen user agent field on /_matrix/client/v3/devices.
- self.msc3852_enabled: bool = experimental.get("msc3852_enabled", False)
-
# MSC3866: M_USER_AWAITING_APPROVAL error code
raw_msc3866_config = experimental.get("msc3866", {})
self.msc3866 = MSC3866Config(**raw_msc3866_config)
diff --git a/synapse/events/utils.py b/synapse/events/utils.py
index b79a68f589..89eb2182af 100644
--- a/synapse/events/utils.py
+++ b/synapse/events/utils.py
@@ -48,7 +48,7 @@ from synapse.api.room_versions import RoomVersion
from synapse.logging.opentracing import SynapseTags, set_tag, trace
from synapse.types import JsonDict, Requester
-from . import EventBase, StrippedStateEvent, make_event_from_dict
+from . import EventBase, FrozenEventV2, StrippedStateEvent, make_event_from_dict
if TYPE_CHECKING:
from synapse.handlers.relations import BundledAggregations
@@ -109,6 +109,14 @@ def clone_event(event: EventBase) -> EventBase:
event.get_dict(), event.room_version, event.internal_metadata.get_dict()
)
+ # Starting FrozenEventV2, the event ID is an (expensive) hash of the event. This is
+ # lazily computed when we get the FrozenEventV2.event_id property, then cached in
+ # _event_id field. Later FrozenEvent formats all inherit from FrozenEventV2, so we
+ # can use the same logic here.
+ if isinstance(event, FrozenEventV2) and isinstance(new_event, FrozenEventV2):
+ # If we already pre-computed the event ID, use it.
+ new_event._event_id = event._event_id
+
# Copy the bits of `internal_metadata` that aren't returned by `get_dict`.
new_event.internal_metadata.stream_ordering = (
event.internal_metadata.stream_ordering
@@ -412,7 +420,7 @@ class SerializeEventConfig:
# Function to convert from federation format to client format
event_format: Callable[[JsonDict], JsonDict] = format_event_for_client_v1
# The entity that requested the event. This is used to determine whether to include
- # the transaction_id in the unsigned section of the event.
+ # the transaction_id and delay_id in the unsigned section of the event.
requester: Requester | None = None
# List of event fields to include. If empty, all fields will be returned.
only_event_fields: list[str] | None = None
@@ -475,44 +483,49 @@ def serialize_event(
config=config,
)
- # If we have a txn_id saved in the internal_metadata, we should include it in the
- # unsigned section of the event if it was sent by the same session as the one
- # requesting the event.
- txn_id: str | None = getattr(e.internal_metadata, "txn_id", None)
- if (
- txn_id is not None
- and config.requester is not None
- and config.requester.user.to_string() == e.sender
- ):
- # Some events do not have the device ID stored in the internal metadata,
- # this includes old events as well as those created by appservice, guests,
- # or with tokens minted with the admin API. For those events, fallback
- # to using the access token instead.
- event_device_id: str | None = getattr(e.internal_metadata, "device_id", None)
- if event_device_id is not None:
- if event_device_id == config.requester.device_id:
- d["unsigned"]["transaction_id"] = txn_id
+ # If we have applicable fields saved in the internal_metadata, include them in the
+ # unsigned section of the event if the event was sent by the same session (or when
+ # appropriate, just the same sender) as the one requesting the event.
+ if config.requester is not None and config.requester.user.to_string() == e.sender:
+ txn_id: str | None = getattr(e.internal_metadata, "txn_id", None)
+ if txn_id is not None:
+ # Some events do not have the device ID stored in the internal metadata,
+ # this includes old events as well as those created by appservice, guests,
+ # or with tokens minted with the admin API. For those events, fallback
+ # to using the access token instead.
+ event_device_id: str | None = getattr(
+ e.internal_metadata, "device_id", None
+ )
+ if event_device_id is not None:
+ if event_device_id == config.requester.device_id:
+ d["unsigned"]["transaction_id"] = txn_id
- else:
- # Fallback behaviour: only include the transaction ID if the event
- # was sent from the same access token.
- #
- # For regular users, the access token ID can be used to determine this.
- # This includes access tokens minted with the admin API.
- #
- # For guests and appservice users, we can't check the access token ID
- # so assume it is the same session.
- event_token_id: int | None = getattr(e.internal_metadata, "token_id", None)
- if (
- (
- event_token_id is not None
- and config.requester.access_token_id is not None
- and event_token_id == config.requester.access_token_id
+ else:
+ # Fallback behaviour: only include the transaction ID if the event
+ # was sent from the same access token.
+ #
+ # For regular users, the access token ID can be used to determine this.
+ # This includes access tokens minted with the admin API.
+ #
+ # For guests and appservice users, we can't check the access token ID
+ # so assume it is the same session.
+ event_token_id: int | None = getattr(
+ e.internal_metadata, "token_id", None
)
- or config.requester.is_guest
- or config.requester.app_service
- ):
- d["unsigned"]["transaction_id"] = txn_id
+ if (
+ (
+ event_token_id is not None
+ and config.requester.access_token_id is not None
+ and event_token_id == config.requester.access_token_id
+ )
+ or config.requester.is_guest
+ or config.requester.app_service
+ ):
+ d["unsigned"]["transaction_id"] = txn_id
+
+ delay_id: str | None = getattr(e.internal_metadata, "delay_id", None)
+ if delay_id is not None:
+ d["unsigned"]["org.matrix.msc4140.delay_id"] = delay_id
# 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
diff --git a/synapse/handlers/delayed_events.py b/synapse/handlers/delayed_events.py
index 7e41716f1e..4a9f646d4d 100644
--- a/synapse/handlers/delayed_events.py
+++ b/synapse/handlers/delayed_events.py
@@ -560,6 +560,7 @@ class DelayedEventsHandler:
action=membership,
content=event.content,
origin_server_ts=event.origin_server_ts,
+ delay_id=event.delay_id,
)
else:
event_dict: JsonDict = {
@@ -585,6 +586,7 @@ class DelayedEventsHandler:
requester,
event_dict,
txn_id=txn_id,
+ delay_id=event.delay_id,
)
event_id = sent_event.event_id
except ShadowBanError:
diff --git a/synapse/handlers/device.py b/synapse/handlers/device.py
index 29074f5e20..9a371651fb 100644
--- a/synapse/handlers/device.py
+++ b/synapse/handlers/device.py
@@ -129,7 +129,6 @@ class DeviceHandler:
self._auth_handler = hs.get_auth_handler()
self._account_data_handler = hs.get_account_data_handler()
self._event_sources = hs.get_event_sources()
- self._msc3852_enabled = hs.config.experimental.msc3852_enabled
self._query_appservices_for_keys = (
hs.config.experimental.msc3984_appservice_key_query
)
diff --git a/synapse/handlers/message.py b/synapse/handlers/message.py
index 99ce120736..eb01622515 100644
--- a/synapse/handlers/message.py
+++ b/synapse/handlers/message.py
@@ -585,6 +585,7 @@ class EventCreationHandler:
state_map: StateMap[str] | None = None,
for_batch: bool = False,
current_state_group: int | None = None,
+ delay_id: str | None = None,
) -> tuple[EventBase, UnpersistedEventContextBase]:
"""
Given a dict from a client, create a new event. If bool for_batch is true, will
@@ -600,7 +601,7 @@ class EventCreationHandler:
Args:
requester
event_dict: An entire event
- txn_id
+ txn_id: The transaction ID.
prev_event_ids:
the forward extremities to use as the prev_events for the
new event.
@@ -639,6 +640,8 @@ class EventCreationHandler:
current_state_group: the current state group, used only for creating events for
batch persisting
+ delay_id: The delay ID of this event, if it was a delayed event.
+
Raises:
ResourceLimitError if server is blocked to some resource being
exceeded
@@ -726,6 +729,9 @@ class EventCreationHandler:
if txn_id is not None:
builder.internal_metadata.txn_id = txn_id
+ if delay_id is not None:
+ builder.internal_metadata.delay_id = delay_id
+
builder.internal_metadata.outlier = outlier
event, unpersisted_context = await self.create_new_client_event(
@@ -966,6 +972,7 @@ class EventCreationHandler:
ignore_shadow_ban: bool = False,
outlier: bool = False,
depth: int | None = None,
+ delay_id: str | None = None,
) -> tuple[EventBase, int]:
"""
Creates an event, then sends it.
@@ -994,6 +1001,7 @@ class EventCreationHandler:
depth: Override the depth used to order the event in the DAG.
Should normally be set to None, which will cause the depth to be calculated
based on the prev_events.
+ delay_id: The delay ID of this event, if it was a delayed event.
Returns:
The event, and its stream ordering (if deduplication happened,
@@ -1090,6 +1098,7 @@ class EventCreationHandler:
ignore_shadow_ban=ignore_shadow_ban,
outlier=outlier,
depth=depth,
+ delay_id=delay_id,
)
async def _create_and_send_nonmember_event_locked(
@@ -1103,6 +1112,7 @@ class EventCreationHandler:
ignore_shadow_ban: bool = False,
outlier: bool = False,
depth: int | None = None,
+ delay_id: str | None = None,
) -> tuple[EventBase, int]:
room_id = event_dict["room_id"]
@@ -1131,6 +1141,7 @@ class EventCreationHandler:
state_event_ids=state_event_ids,
outlier=outlier,
depth=depth,
+ delay_id=delay_id,
)
context = await unpersisted_context.persist(event)
diff --git a/synapse/handlers/room_member.py b/synapse/handlers/room_member.py
index 0c6be72716..b2e678e90e 100644
--- a/synapse/handlers/room_member.py
+++ b/synapse/handlers/room_member.py
@@ -408,6 +408,7 @@ class RoomMemberHandler(metaclass=abc.ABCMeta):
require_consent: bool = True,
outlier: bool = False,
origin_server_ts: int | None = None,
+ delay_id: str | None = None,
) -> tuple[str, int]:
"""
Internal membership update function to get an existing event or create
@@ -440,6 +441,7 @@ class RoomMemberHandler(metaclass=abc.ABCMeta):
opposed to being inline with the current DAG.
origin_server_ts: The origin_server_ts to use if a new event is created. Uses
the current timestamp if set to None.
+ delay_id: The delay ID of this event, if it was a delayed event.
Returns:
Tuple of event ID and stream ordering position
@@ -492,6 +494,7 @@ class RoomMemberHandler(metaclass=abc.ABCMeta):
depth=depth,
require_consent=require_consent,
outlier=outlier,
+ delay_id=delay_id,
)
context = await unpersisted_context.persist(event)
prev_state_ids = await context.get_prev_state_ids(
@@ -587,6 +590,7 @@ class RoomMemberHandler(metaclass=abc.ABCMeta):
state_event_ids: list[str] | None = None,
depth: int | None = None,
origin_server_ts: int | None = None,
+ delay_id: str | None = None,
) -> tuple[str, int]:
"""Update a user's membership in a room.
@@ -617,6 +621,7 @@ class RoomMemberHandler(metaclass=abc.ABCMeta):
based on the prev_events.
origin_server_ts: The origin_server_ts to use if a new event is created. Uses
the current timestamp if set to None.
+ delay_id: The delay ID of this event, if it was a delayed event.
Returns:
A tuple of the new event ID and stream ID.
@@ -679,6 +684,7 @@ class RoomMemberHandler(metaclass=abc.ABCMeta):
state_event_ids=state_event_ids,
depth=depth,
origin_server_ts=origin_server_ts,
+ delay_id=delay_id,
)
return result
@@ -701,6 +707,7 @@ class RoomMemberHandler(metaclass=abc.ABCMeta):
state_event_ids: list[str] | None = None,
depth: int | None = None,
origin_server_ts: int | None = None,
+ delay_id: str | None = None,
) -> tuple[str, int]:
"""Helper for update_membership.
@@ -733,6 +740,7 @@ class RoomMemberHandler(metaclass=abc.ABCMeta):
based on the prev_events.
origin_server_ts: The origin_server_ts to use if a new event is created. Uses
the current timestamp if set to None.
+ delay_id: The delay ID of this event, if it was a delayed event.
Returns:
A tuple of the new event ID and stream ID.
@@ -943,6 +951,7 @@ class RoomMemberHandler(metaclass=abc.ABCMeta):
require_consent=require_consent,
outlier=outlier,
origin_server_ts=origin_server_ts,
+ delay_id=delay_id,
)
latest_event_ids = await self.store.get_prev_events_for_room(room_id)
@@ -1201,6 +1210,7 @@ class RoomMemberHandler(metaclass=abc.ABCMeta):
require_consent=require_consent,
outlier=outlier,
origin_server_ts=origin_server_ts,
+ delay_id=delay_id,
)
async def check_for_any_membership_in_room(
diff --git a/synapse/http/server.py b/synapse/http/server.py
index 226cb00831..2c235e04f4 100644
--- a/synapse/http/server.py
+++ b/synapse/http/server.py
@@ -861,7 +861,18 @@ def respond_with_json(
encoder = _encode_json_bytes
request.setHeader(b"Content-Type", b"application/json")
- request.setHeader(b"Cache-Control", b"no-cache, no-store, must-revalidate")
+ # Insert a default Cache-Control header if the servlet hasn't already set one. The
+ # default directive tells both the client and any intermediary cache to not cache
+ # the response, which is a sensible default to have on most API endpoints.
+ # The absence `Cache-Control` header would mean that it's up to the clients and
+ # caching proxies mood to cache things if they want. This can be dangerous, which is
+ # why we explicitly set a "don't cache by default" policy.
+ # In practice, `no-store` should be enough, but having all three directives is more
+ # conservative in case we encounter weird, non-spec compliant caches.
+ # See https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Cache-Control#directives
+ # for more details.
+ if not request.responseHeaders.hasHeader(b"Cache-Control"):
+ request.setHeader(b"Cache-Control", b"no-cache, no-store, must-revalidate")
if send_cors:
set_cors_headers(request)
@@ -901,7 +912,18 @@ def respond_with_json_bytes(
request.setHeader(b"Content-Type", b"application/json")
request.setHeader(b"Content-Length", b"%d" % (len(json_bytes),))
- request.setHeader(b"Cache-Control", b"no-cache, no-store, must-revalidate")
+ # Insert a default Cache-Control header if the servlet hasn't already set one. The
+ # default directive tells both the client and any intermediary cache to not cache
+ # the response, which is a sensible default to have on most API endpoints.
+ # The absence `Cache-Control` header would mean that it's up to the clients and
+ # caching proxies mood to cache things if they want. This can be dangerous, which is
+ # why we explicitly set a "don't cache by default" policy.
+ # In practice, `no-store` should be enough, but having all three directives is more
+ # conservative in case we encounter weird, non-spec compliant caches.
+ # See https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Cache-Control#directives
+ # for more details.
+ if not request.responseHeaders.hasHeader(b"Cache-Control"):
+ request.setHeader(b"Cache-Control", b"no-cache, no-store, must-revalidate")
if send_cors:
set_cors_headers(request)
diff --git a/synapse/http/site.py b/synapse/http/site.py
index 6ced5b98b3..9b7fd5c936 100644
--- a/synapse/http/site.py
+++ b/synapse/http/site.py
@@ -638,10 +638,12 @@ class SynapseRequest(Request):
if authenticated_entity:
requester = f"{authenticated_entity}|{requester}"
+ # Updates to this log line should also be reflected in our docs,
+ # `docs/usage/administration/request_log.md`
self.synapse_site.access_logger.log(
log_level,
"%s - %s - {%s}"
- " Processed request: %.3fsec/%.3fsec (%.3fsec, %.3fsec) (%.3fsec/%.3fsec/%d)"
+ " Processed request: %.3fsec/%.3fsec ru=(%.3fsec, %.3fsec) db=(%.3fsec/%.3fsec/%d)"
' %sB %s "%s %s %s" "%s" [%d dbevts]',
self.get_client_ip_if_available(),
self.synapse_site.site_tag,
diff --git a/synapse/rest/client/auth_metadata.py b/synapse/rest/client/auth_metadata.py
index 702f550906..062b8ed13e 100644
--- a/synapse/rest/client/auth_metadata.py
+++ b/synapse/rest/client/auth_metadata.py
@@ -49,6 +49,25 @@ class AuthIssuerServlet(RestServlet):
self._auth = hs.get_auth()
async def on_GET(self, request: SynapseRequest) -> tuple[int, JsonDict]:
+ # This endpoint is unauthenticated and the response only depends on
+ # the metadata we get from Matrix Authentication Service. Internally,
+ # MasDelegatedAuth/MSC3861DelegatedAuth.issuer() are already caching the
+ # response in memory anyway. Ideally we would follow any Cache-Control directive
+ # given by MAS, but this is fine for now.
+ #
+ # - `public` means it can be cached both in the browser and in caching proxies
+ # - `max-age` controls how long we cache on the browser side. 10m is sane enough
+ # - `s-maxage` controls how long we cache on the proxy side. Since caching
+ # proxies usually have a way to purge caches, it is fine to cache there for
+ # longer (1h), and issue cache invalidations in case we need it
+ # - `stale-while-revalidate` allows caching proxies to serve stale content while
+ # revalidating in the background. This is useful for making this request always
+ # 'snappy' to end users whilst still keeping it fresh
+ request.setHeader(
+ b"Cache-Control",
+ b"public, max-age=600, s-maxage=3600, stale-while-revalidate=600",
+ )
+
if self._config.mas.enabled:
assert isinstance(self._auth, MasDelegatedAuth)
return 200, {"issuer": await self._auth.issuer()}
@@ -94,6 +113,25 @@ class AuthMetadataServlet(RestServlet):
self._auth = hs.get_auth()
async def on_GET(self, request: SynapseRequest) -> tuple[int, JsonDict]:
+ # This endpoint is unauthenticated and the response only depends on
+ # the metadata we get from Matrix Authentication Service. Internally,
+ # MasDelegatedAuth/MSC3861DelegatedAuth.issuer() are already caching the
+ # response in memory anyway. Ideally we would follow any Cache-Control directive
+ # given by MAS, but this is fine for now.
+ #
+ # - `public` means it can be cached both in the browser and in caching proxies
+ # - `max-age` controls how long we cache on the browser side. 10m is sane enough
+ # - `s-maxage` controls how long we cache on the proxy side. Since caching
+ # proxies usually have a way to purge caches, it is fine to cache there for
+ # longer (1h), and issue cache invalidations in case we need it
+ # - `stale-while-revalidate` allows caching proxies to serve stale content while
+ # revalidating in the background. This is useful for making this request always
+ # 'snappy' to end users whilst still keeping it fresh
+ request.setHeader(
+ b"Cache-Control",
+ b"public, max-age=600, s-maxage=3600, stale-while-revalidate=600",
+ )
+
if self._config.mas.enabled:
assert isinstance(self._auth, MasDelegatedAuth)
return 200, await self._auth.auth_metadata()
diff --git a/synapse/rest/client/devices.py b/synapse/rest/client/devices.py
index b39ca6a483..4b84131d32 100644
--- a/synapse/rest/client/devices.py
+++ b/synapse/rest/client/devices.py
@@ -55,7 +55,6 @@ class DevicesRestServlet(RestServlet):
self.hs = hs
self.auth = hs.get_auth()
self.device_handler = hs.get_device_handler()
- self._msc3852_enabled = hs.config.experimental.msc3852_enabled
async def on_GET(self, request: SynapseRequest) -> tuple[int, JsonDict]:
requester = await self.auth.get_user_by_req(request, allow_guest=True)
@@ -63,17 +62,10 @@ class DevicesRestServlet(RestServlet):
requester.user.to_string()
)
- # If MSC3852 is disabled, then the "last_seen_user_agent" field will be
- # removed from each device. If it is enabled, then the field name will
- # be replaced by the unstable identifier.
- #
- # When MSC3852 is accepted, this block of code can just be removed to
- # expose "last_seen_user_agent" to clients.
for device in devices:
- last_seen_user_agent = device["last_seen_user_agent"]
+ # This field is only for admin access and should not be exposed to clients.
+ # (MSC3852, which is closed, did propose to expose it.).
del device["last_seen_user_agent"]
- if self._msc3852_enabled:
- device["org.matrix.msc3852.last_seen_user_agent"] = last_seen_user_agent
return 200, {"devices": devices}
@@ -144,7 +136,6 @@ class DeviceRestServlet(RestServlet):
handler = hs.get_device_handler()
self.device_handler = handler
self.auth_handler = hs.get_auth_handler()
- self._msc3852_enabled = hs.config.experimental.msc3852_enabled
self._auth_delegation_enabled = (
hs.config.mas.enabled or hs.config.experimental.msc3861.enabled
)
@@ -159,16 +150,9 @@ class DeviceRestServlet(RestServlet):
if device is None:
raise NotFoundError("No device found")
- # If MSC3852 is disabled, then the "last_seen_user_agent" field will be
- # removed from each device. If it is enabled, then the field name will
- # be replaced by the unstable identifier.
- #
- # When MSC3852 is accepted, this block of code can just be removed to
- # expose "last_seen_user_agent" to clients.
- last_seen_user_agent = device["last_seen_user_agent"]
+ # This field is only for admin access and should not be exposed to clients.
+ # (MSC3852, which is closed, did propose to expose it.)
del device["last_seen_user_agent"]
- if self._msc3852_enabled:
- device["org.matrix.msc3852.last_seen_user_agent"] = last_seen_user_agent
return 200, device
diff --git a/synapse/rest/client/versions.py b/synapse/rest/client/versions.py
index 23f5ffeedb..f8d7a1a4d9 100644
--- a/synapse/rest/client/versions.py
+++ b/synapse/rest/client/versions.py
@@ -81,6 +81,26 @@ class VersionsRestServlet(RestServlet):
msc3575_enabled = await self.store.is_feature_enabled(
user_id, ExperimentalFeature.MSC3575
)
+ else:
+ # Allow caching of unauthenticated responses, as they only depend
+ # on server configuration which rarely changes.
+ #
+ # - `public` means it can be cached both in the browser and in caching proxies
+ # - `max-age` controls how long we cache on the browser side. 10m is sane enough
+ # - `s-maxage` controls how long we cache on the proxy side. Since caching
+ # proxies usually have a way to purge caches, it is fine to cache there for
+ # longer (1h), and issue cache invalidations in case we need it
+ # - `stale-while-revalidate` allows caching proxies to serve stale content while
+ # revalidating in the background. This is useful for making this request always
+ # 'snappy' to end users whilst still keeping it fresh
+ request.setHeader(
+ b"Cache-Control",
+ b"public, max-age=600, s-maxage=3600, stale-while-revalidate=600",
+ )
+
+ # Tell caches to vary on the Authorization header, so that
+ # authenticated responses are not served from cache.
+ request.setHeader(b"Vary", b"Authorization")
return (
200,
diff --git a/synapse/rest/synapse/mas/users.py b/synapse/rest/synapse/mas/users.py
index 55c7337555..01db41bcfa 100644
--- a/synapse/rest/synapse/mas/users.py
+++ b/synapse/rest/synapse/mas/users.py
@@ -112,6 +112,18 @@ class MasProvisionUserResource(MasBaseResource):
unset_emails: StrictBool = False
set_emails: list[StrictStr] | None = None
+ locked: StrictBool | None = None
+ """
+ True to lock user. False to unlock. None to leave the same.
+
+ This is mostly for informational purposes; if the user's account is locked in MAS
+ but not in Synapse, the token introspection response will prevent them from using
+ their account.
+
+ However, having a local copy of the locked state in Synapse is useful for excluding
+ the user from the user directory.
+ """
+
@model_validator(mode="before")
@classmethod
def validate_exclusive(cls, values: Any) -> Any:
@@ -206,6 +218,9 @@ class MasProvisionUserResource(MasBaseResource):
validated_at=current_time,
)
+ if body.locked is not None:
+ await self.store.set_user_locked_status(user_id.to_string(), body.locked)
+
if body.unset_avatar_url:
await self.profile_handler.set_avatar_url(
target_user=user_id,
diff --git a/synapse/synapse_rust/events.pyi b/synapse/synapse_rust/events.pyi
index 0add391c65..185f29694b 100644
--- a/synapse/synapse_rust/events.pyi
+++ b/synapse/synapse_rust/events.pyi
@@ -38,6 +38,8 @@ class EventInternalMetadata:
txn_id: str
"""The transaction ID, if it was set when the event was created."""
+ delay_id: str
+ """The delay ID, set only if the event was a delayed event."""
token_id: int
"""The access token ID of the user who sent this event, if any."""
device_id: str
diff --git a/tests/rest/client/test_delayed_events.py b/tests/rest/client/test_delayed_events.py
index efa69a393a..da904ce1f5 100644
--- a/tests/rest/client/test_delayed_events.py
+++ b/tests/rest/client/test_delayed_events.py
@@ -22,7 +22,7 @@ from twisted.internet.testing import MemoryReactor
from synapse.api.errors import Codes
from synapse.rest import admin
-from synapse.rest.client import delayed_events, login, room, versions
+from synapse.rest.client import delayed_events, login, room, sync, versions
from synapse.server import HomeServer
from synapse.types import JsonDict
from synapse.util.clock import Clock
@@ -59,6 +59,7 @@ class DelayedEventsTestCase(HomeserverTestCase):
delayed_events.register_servlets,
login.register_servlets,
room.register_servlets,
+ sync.register_servlets,
]
def default_config(self) -> JsonDict:
@@ -106,6 +107,9 @@ class DelayedEventsTestCase(HomeserverTestCase):
self.user1_access_token,
)
self.assertEqual(HTTPStatus.OK, channel.code, channel.result)
+ delay_id = channel.json_body.get("delay_id")
+ assert delay_id is not None
+
events = self._get_delayed_events()
self.assertEqual(1, len(events), events)
content = self._get_delayed_event_content(events[0])
@@ -128,6 +132,56 @@ class DelayedEventsTestCase(HomeserverTestCase):
)
self.assertEqual(setter_expected, content.get(setter_key), content)
+ self._find_sent_delayed_event(self.user1_access_token, delay_id, True)
+ self._find_sent_delayed_event(self.user2_access_token, delay_id, False)
+
+ def test_delayed_member_events_are_sent_on_timeout(self) -> None:
+ channel = self.make_request(
+ "PUT",
+ _get_path_for_delayed_state(
+ self.room_id,
+ "m.room.member",
+ self.user2_user_id,
+ 900,
+ ),
+ {
+ "membership": "leave",
+ "reason": "Delayed kick",
+ },
+ self.user1_access_token,
+ )
+ self.assertEqual(HTTPStatus.OK, channel.code, channel.result)
+ delay_id = channel.json_body.get("delay_id")
+ assert delay_id is not None
+
+ events = self._get_delayed_events()
+ self.assertEqual(1, len(events), events)
+ content = self._get_delayed_event_content(events[0])
+ self.assertEqual("leave", content.get("membership"), content)
+ self.assertEqual("Delayed kick", content.get("reason"), content)
+
+ content = self.helper.get_state(
+ self.room_id,
+ "m.room.member",
+ self.user1_access_token,
+ state_key=self.user2_user_id,
+ )
+ self.assertEqual("join", content.get("membership"), content)
+
+ self.reactor.advance(1)
+ self.assertListEqual([], self._get_delayed_events())
+ content = self.helper.get_state(
+ self.room_id,
+ "m.room.member",
+ self.user1_access_token,
+ state_key=self.user2_user_id,
+ )
+ self.assertEqual("leave", content.get("membership"), content)
+ self.assertEqual("Delayed kick", content.get("reason"), content)
+
+ self._find_sent_delayed_event(self.user1_access_token, delay_id, True)
+ self._find_sent_delayed_event(self.user2_access_token, delay_id, False)
+
def test_get_delayed_events_auth(self) -> None:
channel = self.make_request("GET", PATH_PREFIX)
self.assertEqual(HTTPStatus.UNAUTHORIZED, channel.code, channel.result)
@@ -254,6 +308,9 @@ class DelayedEventsTestCase(HomeserverTestCase):
expect_code=HTTPStatus.NOT_FOUND,
)
+ self._find_sent_delayed_event(self.user1_access_token, delay_id, False)
+ self._find_sent_delayed_event(self.user2_access_token, delay_id, False)
+
@parameterized.expand((True, False))
@unittest.override_config(
{"rc_delayed_event_mgmt": {"per_second": 0.5, "burst_count": 1}}
@@ -327,6 +384,9 @@ class DelayedEventsTestCase(HomeserverTestCase):
)
self.assertEqual(content_value, content.get(content_property_name), content)
+ self._find_sent_delayed_event(self.user1_access_token, delay_id, True)
+ self._find_sent_delayed_event(self.user2_access_token, delay_id, False)
+
@parameterized.expand((True, False))
@unittest.override_config({"rc_message": {"per_second": 2.5, "burst_count": 3}})
def test_send_delayed_event_ratelimit(self, action_in_path: bool) -> None:
@@ -406,6 +466,9 @@ class DelayedEventsTestCase(HomeserverTestCase):
)
self.assertEqual(setter_expected, content.get(setter_key), content)
+ self._find_sent_delayed_event(self.user1_access_token, delay_id, True)
+ self._find_sent_delayed_event(self.user2_access_token, delay_id, False)
+
@parameterized.expand((True, False))
@unittest.override_config(
{"rc_delayed_event_mgmt": {"per_second": 0.5, "burst_count": 1}}
@@ -450,6 +513,8 @@ class DelayedEventsTestCase(HomeserverTestCase):
self.user1_access_token,
)
self.assertEqual(HTTPStatus.OK, channel.code, channel.result)
+ delay_id = channel.json_body.get("delay_id")
+ assert delay_id is not None
events = self._get_delayed_events()
self.assertEqual(1, len(events), events)
@@ -474,6 +539,9 @@ class DelayedEventsTestCase(HomeserverTestCase):
)
self.assertEqual(setter_expected, content.get(setter_key), content)
+ self._find_sent_delayed_event(self.user1_access_token, delay_id, True)
+ self._find_sent_delayed_event(self.user2_access_token, delay_id, False)
+
def test_delayed_state_is_cancelled_by_new_state_from_other_user(
self,
) -> None:
@@ -489,6 +557,8 @@ class DelayedEventsTestCase(HomeserverTestCase):
self.user1_access_token,
)
self.assertEqual(HTTPStatus.OK, channel.code, channel.result)
+ delay_id = channel.json_body.get("delay_id")
+ assert delay_id is not None
events = self._get_delayed_events()
self.assertEqual(1, len(events), events)
@@ -513,6 +583,9 @@ class DelayedEventsTestCase(HomeserverTestCase):
)
self.assertEqual(setter_expected, content.get(setter_key), content)
+ self._find_sent_delayed_event(self.user1_access_token, delay_id, False)
+ self._find_sent_delayed_event(self.user2_access_token, delay_id, False)
+
def _get_delayed_events(self) -> list[JsonDict]:
channel = self.make_request(
"GET",
@@ -549,6 +622,39 @@ class DelayedEventsTestCase(HomeserverTestCase):
body["action"] = action
return self.make_request("POST", path, body)
+ def _find_sent_delayed_event(
+ self, access_token: str, delay_id: str, should_find: bool
+ ) -> None:
+ """Call /sync and look for a synced event with a specified delay_id.
+ At most one event will ever have a matching delay_id.
+
+ Args:
+ access_token: The access token of the user to call /sync for.
+ delay_id: The delay_id to search for in synced events.
+ should_find: Whether /sync should include an event with a matching delay_id.
+ """
+ channel = self.make_request("GET", "/sync", access_token=access_token)
+ self.assertEqual(HTTPStatus.OK, channel.code)
+
+ rooms = channel.json_body["rooms"]
+ events = []
+ for membership in "join", "leave":
+ if membership in rooms:
+ events += rooms[membership][self.room_id]["timeline"]["events"]
+
+ found = False
+ for event in events:
+ if event["unsigned"].get("org.matrix.msc4140.delay_id") == delay_id:
+ if not should_find:
+ self.fail(
+ "Found event with matching delay_id, but expected to not find one"
+ )
+ if found:
+ self.fail("Found multiple events with matching delay_id")
+ found = True
+ if should_find and not found:
+ self.fail("Did not find event with matching delay_id")
+
def _get_path_for_delayed_state(
room_id: str, event_type: str, state_key: str, delay_ms: int
diff --git a/tests/rest/synapse/mas/test_users.py b/tests/rest/synapse/mas/test_users.py
index f0f26a939c..6f44761bb8 100644
--- a/tests/rest/synapse/mas/test_users.py
+++ b/tests/rest/synapse/mas/test_users.py
@@ -362,6 +362,64 @@ class MasProvisionUserResource(BaseTestCase):
)
self.assertEqual(channel.code, 400, f"Should fail for content: {content}")
+ def test_lock_and_unlock(self) -> None:
+ store = self.hs.get_datastores().main
+
+ # Create a user in the locked state
+ alice = UserID("alice", "test")
+ channel = self.make_request(
+ "POST",
+ "/_synapse/mas/provision_user",
+ shorthand=False,
+ access_token=self.SHARED_SECRET,
+ content={
+ "localpart": alice.localpart,
+ "locked": True,
+ },
+ )
+ # This created the user, hence the 201 status code
+ self.assertEqual(channel.code, 201, channel.json_body)
+ self.assertEqual(channel.json_body, {})
+ self.assertTrue(
+ self.get_success(store.get_user_locked_status(alice.to_string()))
+ )
+
+ # Then transition from locked to unlocked
+ channel = self.make_request(
+ "POST",
+ "/_synapse/mas/provision_user",
+ shorthand=False,
+ access_token=self.SHARED_SECRET,
+ content={
+ "localpart": alice.localpart,
+ "locked": False,
+ },
+ )
+ # This updated the user, hence the 200 status code
+ self.assertEqual(channel.code, 200, channel.json_body)
+ self.assertEqual(channel.json_body, {})
+ self.assertFalse(
+ self.get_success(store.get_user_locked_status(alice.to_string()))
+ )
+
+ # And back from unlocked to locked
+ channel = self.make_request(
+ "POST",
+ "/_synapse/mas/provision_user",
+ shorthand=False,
+ access_token=self.SHARED_SECRET,
+ content={
+ "localpart": alice.localpart,
+ "locked": True,
+ },
+ )
+ # This updated the user, hence the 200 status code
+ self.assertEqual(channel.code, 200, channel.json_body)
+ self.assertEqual(channel.json_body, {})
+ self.assertTrue(
+ self.get_success(store.get_user_locked_status(alice.to_string()))
+ )
+
@skip_unless(HAS_AUTHLIB, "requires authlib")
class MasIsLocalpartAvailableResource(BaseTestCase):