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):