From e59e5490941c078c1b74fe193f8d7d670ac6f58f Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 2 Feb 2026 23:22:15 +0000 Subject: [PATCH 001/121] Bump actions/setup-go from 6.1.0 to 6.2.0 in the minor-and-patches group (#19423) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps the minor-and-patches group with 1 update: [actions/setup-go](https://github.com/actions/setup-go). Updates `actions/setup-go` from 6.1.0 to 6.2.0
Release notes

Sourced from actions/setup-go's releases.

v6.2.0

What's Changed

Enhancements

Dependency updates

New Contributors

Full Changelog: https://github.com/actions/setup-go/compare/v6...v6.2.0

Commits

[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=actions/setup-go&package-manager=github_actions&previous-version=6.1.0&new-version=6.2.0)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores) Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`. [//]: # (dependabot-automerge-start) [//]: # (dependabot-automerge-end) ---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot merge` will merge this PR after your CI passes on it - `@dependabot squash and merge` will squash and merge this PR after your CI passes on it - `@dependabot cancel merge` will cancel a previously requested merge and block automerging - `@dependabot reopen` will reopen this PR if it is closed - `@dependabot close` will close this PR and stop Dependabot recreating it. You can achieve the same result by closing it manually - `@dependabot show ignore conditions` will show all of the ignore conditions of the specified dependency - `@dependabot ignore major version` will close this group update PR and stop Dependabot creating any more for the specific dependency's major version (unless you unignore this specific dependency's major version or upgrade to it yourself) - `@dependabot ignore minor version` will close this group update PR and stop Dependabot creating any more for the specific dependency's minor version (unless you unignore this specific dependency's minor version or upgrade to it yourself) - `@dependabot ignore ` will close this group update PR and stop Dependabot creating any more for the specific dependency (unless you unignore this specific dependency or upgrade to it yourself) - `@dependabot unignore ` will remove all of the ignore conditions of the specified dependency - `@dependabot unignore ` will remove the ignore condition of the specified dependency and ignore conditions
Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/latest_deps.yml | 2 +- .github/workflows/tests.yml | 2 +- .github/workflows/twisted_trunk.yml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/latest_deps.yml b/.github/workflows/latest_deps.yml index a85551854c0..9e0f2c384e9 100644 --- a/.github/workflows/latest_deps.yml +++ b/.github/workflows/latest_deps.yml @@ -209,7 +209,7 @@ jobs: - name: Prepare Complement's Prerequisites run: synapse/.ci/scripts/setup_complement_prerequisites.sh - - uses: actions/setup-go@4dc6199c7b1a012772edbd06daecab0f50c9053c # v6.1.0 + - uses: actions/setup-go@7a3fe6cf4cb3a834922a1244abfce67bcef6a0c5 # v6.2.0 with: cache-dependency-path: complement/go.sum go-version-file: complement/go.mod diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index fc544fcfde2..bb2e167e234 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -702,7 +702,7 @@ jobs: - name: Prepare Complement's Prerequisites run: synapse/.ci/scripts/setup_complement_prerequisites.sh - - uses: actions/setup-go@4dc6199c7b1a012772edbd06daecab0f50c9053c # v6.1.0 + - uses: actions/setup-go@7a3fe6cf4cb3a834922a1244abfce67bcef6a0c5 # v6.2.0 with: cache-dependency-path: complement/go.sum go-version-file: complement/go.mod diff --git a/.github/workflows/twisted_trunk.yml b/.github/workflows/twisted_trunk.yml index 14b48317dbd..bd5c79f16dc 100644 --- a/.github/workflows/twisted_trunk.yml +++ b/.github/workflows/twisted_trunk.yml @@ -182,7 +182,7 @@ jobs: - name: Prepare Complement's Prerequisites run: synapse/.ci/scripts/setup_complement_prerequisites.sh - - uses: actions/setup-go@4dc6199c7b1a012772edbd06daecab0f50c9053c # v6.1.0 + - uses: actions/setup-go@7a3fe6cf4cb3a834922a1244abfce67bcef6a0c5 # v6.2.0 with: cache-dependency-path: complement/go.sum go-version-file: complement/go.mod From 065ff194c23ca9c326460ace5b55edc0e5eff3d7 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 3 Feb 2026 00:19:25 +0000 Subject: [PATCH 002/121] Bump serde_json from 1.0.145 to 1.0.148 in the patches group across 1 directory (#19391) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps the patches group with 1 update in the / directory: [serde_json](https://github.com/serde-rs/json). Updates `serde_json` from 1.0.145 to 1.0.148
Release notes

Sourced from serde_json's releases.

v1.0.148

  • Update zmij dependency to 1.0

v1.0.147

  • Switch float-to-string algorithm from Ryū to Żmij for better f32 and f64 serialization performance (#1304)

v1.0.146

Commits

[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=serde_json&package-manager=cargo&previous-version=1.0.145&new-version=1.0.148)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores) Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`. [//]: # (dependabot-automerge-start) [//]: # (dependabot-automerge-end) ---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot merge` will merge this PR after your CI passes on it - `@dependabot squash and merge` will squash and merge this PR after your CI passes on it - `@dependabot cancel merge` will cancel a previously requested merge and block automerging - `@dependabot reopen` will reopen this PR if it is closed - `@dependabot close` will close this PR and stop Dependabot recreating it. You can achieve the same result by closing it manually - `@dependabot show ignore conditions` will show all of the ignore conditions of the specified dependency - `@dependabot ignore major version` will close this group update PR and stop Dependabot creating any more for the specific dependency's major version (unless you unignore this specific dependency's major version or upgrade to it yourself) - `@dependabot ignore minor version` will close this group update PR and stop Dependabot creating any more for the specific dependency's minor version (unless you unignore this specific dependency's minor version or upgrade to it yourself) - `@dependabot ignore ` will close this group update PR and stop Dependabot creating any more for the specific dependency (unless you unignore this specific dependency or upgrade to it yourself) - `@dependabot unignore ` will remove all of the ignore conditions of the specified dependency - `@dependabot unignore ` will remove the ignore condition of the specified dependency and ignore conditions
--------- Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Devon Hudson --- .github/workflows/tests.yml | 2 +- Cargo.lock | 12 +++++++++--- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index bb2e167e234..715dfa93d9a 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -238,7 +238,7 @@ jobs: - name: Install Rust uses: dtolnay/rust-toolchain@e97e2d8cc328f1b50210efc529dca0028893a2d9 # master with: - toolchain: nightly-2025-04-23 + toolchain: nightly-2026-02-01 components: clippy - uses: Swatinem/rust-cache@779680da715d629ac1d338a641029a2f4372abb5 # v2.8.2 diff --git a/Cargo.lock b/Cargo.lock index 0edfef68697..8d1cd967d5c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1207,15 +1207,15 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.145" +version = "1.0.149" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "402a6f66d8c709116cf22f558eab210f5a50187f702eb4d7e5ef38d9a7f1c79c" +checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86" dependencies = [ "itoa", "memchr", - "ryu", "serde", "serde_core", + "zmij", ] [[package]] @@ -1921,3 +1921,9 @@ dependencies = [ "quote", "syn", ] + +[[package]] +name = "zmij" +version = "1.0.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ff05f8caa9038894637571ae6b9e29466c1f4f829d26c9b28f869a29cbe3445" From 84d591934ba27054fb42e807928df0901ac1eea7 Mon Sep 17 00:00:00 2001 From: Olivier 'reivilibre Date: Tue, 3 Feb 2026 15:33:39 +0000 Subject: [PATCH 003/121] Add notes that new experimental features should have associated tracking issues. (#19410) Signed-off-by: Olivier 'reivilibre --- changelog.d/19410.misc | 1 + docs/development/experimental_features.md | 27 +++++++++++++++++++++++ synapse/config/experimental.py | 6 ++++- 3 files changed, 33 insertions(+), 1 deletion(-) create mode 100644 changelog.d/19410.misc diff --git a/changelog.d/19410.misc b/changelog.d/19410.misc new file mode 100644 index 00000000000..97a4070304a --- /dev/null +++ b/changelog.d/19410.misc @@ -0,0 +1 @@ +Add notes that new experimental features should have associated tracking issues. \ No newline at end of file diff --git a/docs/development/experimental_features.md b/docs/development/experimental_features.md index d6b11496cc4..5a86017ecfd 100644 --- a/docs/development/experimental_features.md +++ b/docs/development/experimental_features.md @@ -35,3 +35,30 @@ but one should be used if unsure. New experimental configuration flags should be added under the `experimental` configuration key (see the `synapse.config.experimental` file) and either explain (briefly) what is being enabled, or include the MSC number. +The configuration flag should link to the tracking issue for the experimental feature (see below). + + +## Tracking issues for experimental features + +In the interest of having some documentation around experimental features, without +polluting the stable documentation, all new experimental features should have a tracking issue with +[the `T-ExperimentalFeature` label](https://github.com/element-hq/synapse/issues?q=sort%3Aupdated-desc+state%3Aopen+label%3A%22T-ExperimentalFeature%22), +kept open as long as the experimental feature is present in Synapse. + +The configuration option for the feature should have a comment linking to the tracking issue, +for ease of discoverability. + +As a guideline, the issue should contain: + +- Context for why this experimental feature is in Synapse + - This could well be a link to somewhere else, where this context is already available. +- If applicable, why the feature is enabled by default. (Why do we need to enable it by default and why is it safe?) +- If applicable, setup instructions for any non-standard components or configuration needed by the feature. + (Ideally this will be moved to the configuration manual after stabilisation.) +- Design decisions behind the Synapse implementation. + (Ideally this will be moved to the developers' documentation after stabilisation.) +- Any caveats around the current implementation of the feature, such as: + - missing aspects + - breakage or incompatibility that is expected if/when the feature is stabilised, + or when the feature is turned on/off +- Criteria for how we know whether we can remove the feature in the future. diff --git a/synapse/config/experimental.py b/synapse/config/experimental.py index 0150b71621c..1bd70ead09a 100644 --- a/synapse/config/experimental.py +++ b/synapse/config/experimental.py @@ -366,7 +366,11 @@ class MSC3866Config: class ExperimentalConfig(Config): - """Config section for enabling experimental features""" + """Config section for enabling experimental features + + All new experimental features should have a tracking issue with the + `T-ExperimentalFeatures` label, kept open as long as the experimental + feature is present in Synapse.""" section = "experimental" From 98a540a41d6fd5920fad2bab900425e869fb9cbd Mon Sep 17 00:00:00 2001 From: Renaud Allard Date: Tue, 3 Feb 2026 16:40:20 +0100 Subject: [PATCH 004/121] Fix a typo in check_dependencies.py which makes setuptools_rust a running dependency (#19417) ### Pull Request Checklist * [x] Pull request is based on the develop branch * [x] [Code style](https://element-hq.github.io/synapse/latest/code_style.html) is correct (run the [linters](https://element-hq.github.io/synapse/latest/development/contributing_guide.html#run-the-linters)) There is a typo in check_dependencies.py which makes setuptools_rust a runtime requirement, but there is no need for it at runtime. This patch solves the typo. I tested starting 1.146.0 with this patch and without setuptools_rust and it starts correctly --- changelog.d/19417.bugfix | 1 + synapse/util/check_dependencies.py | 3 ++- tests/util/test_check_dependencies.py | 6 +++--- 3 files changed, 6 insertions(+), 4 deletions(-) create mode 100644 changelog.d/19417.bugfix diff --git a/changelog.d/19417.bugfix b/changelog.d/19417.bugfix new file mode 100644 index 00000000000..9f5c9c02d94 --- /dev/null +++ b/changelog.d/19417.bugfix @@ -0,0 +1 @@ +Fix a typo that incorrectly made `setuptools_rust` a runtime dependency. diff --git a/synapse/util/check_dependencies.py b/synapse/util/check_dependencies.py index 7e92b555928..cf7573c99d3 100644 --- a/synapse/util/check_dependencies.py +++ b/synapse/util/check_dependencies.py @@ -32,6 +32,7 @@ from packaging.markers import Marker, Value, Variable, default_environment from packaging.requirements import Requirement +from packaging.utils import canonicalize_name DISTRIBUTION_NAME = "matrix-synapse" @@ -96,7 +97,7 @@ def _should_ignore_runtime_requirement(req: Requirement) -> bool: # In any case, workaround this by ignoring setuptools_rust here. (It might be # slightly cleaner to put `setuptools_rust` in a `build` extra or similar, but for # now let's do something quick and dirty. - if req.name == "setuptools_rust": + if canonicalize_name(req.name) == "setuptools-rust": return True return False diff --git a/tests/util/test_check_dependencies.py b/tests/util/test_check_dependencies.py index b7a23dcd9da..eed0519c447 100644 --- a/tests/util/test_check_dependencies.py +++ b/tests/util/test_check_dependencies.py @@ -201,13 +201,13 @@ def test_setuptools_rust_ignored(self) -> None: """ with patch( "synapse.util.check_dependencies.metadata.requires", - return_value=["setuptools_rust >= 1.3"], + return_value=["setuptools-rust >= 1.3"], ): with self.mock_installed_package(None): - # should not raise, even if setuptools_rust is not installed + # should not raise, even if setuptools-rust is not installed check_requirements() with self.mock_installed_package(old): - # We also ignore old versions of setuptools_rust + # We also ignore old versions of setuptools-rust check_requirements() def test_python_version_markers_respected(self) -> None: From 3048ff8b262995254e3ff2c07de3474d5bfa6daf Mon Sep 17 00:00:00 2001 From: Devon Hudson Date: Tue, 3 Feb 2026 08:56:37 -0700 Subject: [PATCH 005/121] 1.147.0rc1 --- CHANGES.md | 20 ++++++++++++++++++++ changelog.d/19306.misc | 1 - changelog.d/19399.misc | 1 - changelog.d/19400.misc | 1 - changelog.d/19402.misc | 1 - changelog.d/19405.misc | 1 - changelog.d/19410.misc | 1 - changelog.d/19412.misc | 1 - changelog.d/19416.bugfix | 1 - changelog.d/19417.bugfix | 1 - debian/changelog | 6 ++++++ pyproject.toml | 2 +- schema/synapse-config.schema.yaml | 2 +- 13 files changed, 28 insertions(+), 11 deletions(-) delete mode 100644 changelog.d/19306.misc delete mode 100644 changelog.d/19399.misc delete mode 100644 changelog.d/19400.misc delete mode 100644 changelog.d/19402.misc delete mode 100644 changelog.d/19405.misc delete mode 100644 changelog.d/19410.misc delete mode 100644 changelog.d/19412.misc delete mode 100644 changelog.d/19416.bugfix delete mode 100644 changelog.d/19417.bugfix diff --git a/CHANGES.md b/CHANGES.md index 516ac4dbfaa..9c688402ef0 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,3 +1,23 @@ +# Synapse 1.147.0rc1 (2026-02-03) + +## Bugfixes + +- Fix memory leak caused by not cleaning up stopped looping calls. Introduced in v1.140.0. ([\#19416](https://github.com/element-hq/synapse/issues/19416)) +- Fix a typo that incorrectly made `setuptools_rust` a runtime dependency. ([\#19417](https://github.com/element-hq/synapse/issues/19417)) + +## Internal Changes + +- Prune stale entries from `sliding_sync_connection_required_state` table. ([\#19306](https://github.com/element-hq/synapse/issues/19306)) +- Update "Event Send Time Quantiles" graph to only use dots for the event persistence rate (Grafana dashboard). ([\#19399](https://github.com/element-hq/synapse/issues/19399)) +- Update and align Grafana dashboard to use regex matching for `job` selectors (`job=~"$job"`) so the "all" value works correctly across all panels. ([\#19400](https://github.com/element-hq/synapse/issues/19400)) +- Don't retry joining partial state rooms all at once on startup. ([\#19402](https://github.com/element-hq/synapse/issues/19402)) +- Disallow requests to the health endpoint from containing trailing path characters. ([\#19405](https://github.com/element-hq/synapse/issues/19405)) +- Add notes that new experimental features should have associated tracking issues. ([\#19410](https://github.com/element-hq/synapse/issues/19410)) +- Bump `pyo3` from 0.26.0 to 0.27.2 and `pythonize` from 0.26.0 to 0.27.0. Contributed by @razvp @ ERCOM. ([\#19412](https://github.com/element-hq/synapse/issues/19412)) + + + + # Synapse 1.146.0 (2026-01-27) No significant changes since 1.146.0rc1. diff --git a/changelog.d/19306.misc b/changelog.d/19306.misc deleted file mode 100644 index 463f87eac3a..00000000000 --- a/changelog.d/19306.misc +++ /dev/null @@ -1 +0,0 @@ -Prune stale entries from `sliding_sync_connection_required_state` table. diff --git a/changelog.d/19399.misc b/changelog.d/19399.misc deleted file mode 100644 index 0d02904f407..00000000000 --- a/changelog.d/19399.misc +++ /dev/null @@ -1 +0,0 @@ -Update "Event Send Time Quantiles" graph to only use dots for the event persistence rate (Grafana dashboard). diff --git a/changelog.d/19400.misc b/changelog.d/19400.misc deleted file mode 100644 index 33b0cb509c5..00000000000 --- a/changelog.d/19400.misc +++ /dev/null @@ -1 +0,0 @@ -Update and align Grafana dashboard to use regex matching for `job` selectors (`job=~"$job"`) so the "all" value works correctly across all panels. diff --git a/changelog.d/19402.misc b/changelog.d/19402.misc deleted file mode 100644 index 0e1ee104a78..00000000000 --- a/changelog.d/19402.misc +++ /dev/null @@ -1 +0,0 @@ -Don't retry joining partial state rooms all at once on startup. diff --git a/changelog.d/19405.misc b/changelog.d/19405.misc deleted file mode 100644 index f3be5b20274..00000000000 --- a/changelog.d/19405.misc +++ /dev/null @@ -1 +0,0 @@ -Disallow requests to the health endpoint from containing trailing path characters. \ No newline at end of file diff --git a/changelog.d/19410.misc b/changelog.d/19410.misc deleted file mode 100644 index 97a4070304a..00000000000 --- a/changelog.d/19410.misc +++ /dev/null @@ -1 +0,0 @@ -Add notes that new experimental features should have associated tracking issues. \ No newline at end of file diff --git a/changelog.d/19412.misc b/changelog.d/19412.misc deleted file mode 100644 index 6b811be799a..00000000000 --- a/changelog.d/19412.misc +++ /dev/null @@ -1 +0,0 @@ -Bump `pyo3` from 0.26.0 to 0.27.2 and `pythonize` from 0.26.0 to 0.27.0. Contributed by @razvp @ ERCOM. \ No newline at end of file diff --git a/changelog.d/19416.bugfix b/changelog.d/19416.bugfix deleted file mode 100644 index f0c2872410a..00000000000 --- a/changelog.d/19416.bugfix +++ /dev/null @@ -1 +0,0 @@ -Fix memory leak caused by not cleaning up stopped looping calls. Introduced in v1.140.0. diff --git a/changelog.d/19417.bugfix b/changelog.d/19417.bugfix deleted file mode 100644 index 9f5c9c02d94..00000000000 --- a/changelog.d/19417.bugfix +++ /dev/null @@ -1 +0,0 @@ -Fix a typo that incorrectly made `setuptools_rust` a runtime dependency. diff --git a/debian/changelog b/debian/changelog index ac013ba1b8e..8168bde3a21 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,3 +1,9 @@ +matrix-synapse-py3 (1.147.0~rc1) stable; urgency=medium + + * New Synapse release 1.147.0rc1. + + -- Synapse Packaging team Tue, 03 Feb 2026 08:53:17 -0700 + matrix-synapse-py3 (1.146.0) stable; urgency=medium * New Synapse release 1.146.0. diff --git a/pyproject.toml b/pyproject.toml index d61f7177bdc..a26247ab1eb 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "matrix-synapse" -version = "1.146.0" +version = "1.147.0rc1" description = "Homeserver for the Matrix decentralised comms protocol" readme = "README.rst" authors = [ diff --git a/schema/synapse-config.schema.yaml b/schema/synapse-config.schema.yaml index 3ed71967528..99c9b6e9bd1 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.146/synapse-config.schema.json +$id: https://element-hq.github.io/synapse/schema/synapse/v1.147/synapse-config.schema.json type: object properties: modules: From 292e1aabdddddb38c054d71c90fe6021a6562eec Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 3 Feb 2026 22:27:18 +0000 Subject: [PATCH 006/121] Bump bytes from 1.11.0 to 1.11.1 (#19432) Bumps [bytes](https://github.com/tokio-rs/bytes) from 1.11.0 to 1.11.1.
Release notes

Sourced from bytes's releases.

Bytes v1.11.1

1.11.1 (February 3rd, 2026)

  • Fix integer overflow in BytesMut::reserve
Changelog

Sourced from bytes's changelog.

1.11.1 (February 3rd, 2026)

  • Fix integer overflow in BytesMut::reserve
Commits

[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=bytes&package-manager=cargo&previous-version=1.11.0&new-version=1.11.1)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores) Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`. [//]: # (dependabot-automerge-start) [//]: # (dependabot-automerge-end) ---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot merge` will merge this PR after your CI passes on it - `@dependabot squash and merge` will squash and merge this PR after your CI passes on it - `@dependabot cancel merge` will cancel a previously requested merge and block automerging - `@dependabot reopen` will reopen this PR if it is closed - `@dependabot close` will close this PR and stop Dependabot recreating it. You can achieve the same result by closing it manually - `@dependabot show ignore conditions` will show all of the ignore conditions of the specified dependency - `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself) You can disable automated security fix PRs for this repo from the [Security Alerts page](https://github.com/element-hq/synapse/network/alerts).
Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- Cargo.lock | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 8d1cd967d5c..e1344f89d61 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -73,9 +73,9 @@ checksum = "46c5e41b57b8bba42a04676d81cb89e9ee8e859a1a66f80a5a72e1cb76b34d43" [[package]] name = "bytes" -version = "1.11.0" +version = "1.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b35204fbdc0b3f4446b89fc1ac2cf84a8a68971995d0bf2e925ec7cd960f9cb3" +checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33" [[package]] name = "cc" From 8d4ebc27c246de1f3088b0b0fb8a1162ec619457 Mon Sep 17 00:00:00 2001 From: Hugh Nimmo-Smith Date: Wed, 4 Feb 2026 17:50:47 +0000 Subject: [PATCH 007/121] Fix reference to the experimental_features section of config in the docs (#19435) --- changelog.d/19435.doc | 1 + docs/development/experimental_features.md | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) create mode 100644 changelog.d/19435.doc diff --git a/changelog.d/19435.doc b/changelog.d/19435.doc new file mode 100644 index 00000000000..4edf2f73dce --- /dev/null +++ b/changelog.d/19435.doc @@ -0,0 +1 @@ +Fix reference to the `experimental_features` section of config. diff --git a/docs/development/experimental_features.md b/docs/development/experimental_features.md index 5a86017ecfd..a852fb88fd4 100644 --- a/docs/development/experimental_features.md +++ b/docs/development/experimental_features.md @@ -32,7 +32,7 @@ expected and not an issue. It is not a requirement for experimental features to be behind a configuration flag, but one should be used if unsure. -New experimental configuration flags should be added under the `experimental` +New experimental configuration flags should be added under the `experimental_features` configuration key (see the `synapse.config.experimental` file) and either explain (briefly) what is being enabled, or include the MSC number. The configuration flag should link to the tracking issue for the experimental feature (see below). From f6105b73f08dd8a907687990a0a42eaf77dfc3cf Mon Sep 17 00:00:00 2001 From: Olivier 'reivilibre Date: Thu, 5 Feb 2026 11:01:15 +0000 Subject: [PATCH 008/121] Remove support for MSC3244: Room version capabilities as the MSC was rejected. (#19429) Signed-off-by: Olivier 'reivilibre --- changelog.d/19429.removal | 1 + synapse/api/room_versions.py | 38 ------------------------ synapse/config/experimental.py | 3 -- synapse/rest/client/capabilities.py | 7 +---- tests/rest/client/test_capabilities.py | 40 -------------------------- 5 files changed, 2 insertions(+), 87 deletions(-) create mode 100644 changelog.d/19429.removal diff --git a/changelog.d/19429.removal b/changelog.d/19429.removal new file mode 100644 index 00000000000..b52ca2423d4 --- /dev/null +++ b/changelog.d/19429.removal @@ -0,0 +1 @@ +Remove support for [MSC3244: Room version capabilities](https://github.com/matrix-org/matrix-spec-proposals/pull/3244) as the MSC was rejected. \ No newline at end of file diff --git a/synapse/api/room_versions.py b/synapse/api/room_versions.py index 97dac661a37..2f98d7a8a86 100644 --- a/synapse/api/room_versions.py +++ b/synapse/api/room_versions.py @@ -18,7 +18,6 @@ # # -from typing import Callable import attr @@ -496,40 +495,3 @@ class RoomVersions: RoomVersions.HydraV11, ) } - - -@attr.s(slots=True, frozen=True, auto_attribs=True) -class RoomVersionCapability: - """An object which describes the unique attributes of a room version.""" - - identifier: str # the identifier for this capability - preferred_version: RoomVersion | None - support_check_lambda: Callable[[RoomVersion], bool] - - -MSC3244_CAPABILITIES = { - cap.identifier: { - "preferred": ( - cap.preferred_version.identifier - if cap.preferred_version is not None - else None - ), - "support": [ - v.identifier - for v in KNOWN_ROOM_VERSIONS.values() - if cap.support_check_lambda(v) - ], - } - for cap in ( - RoomVersionCapability( - "knock", - RoomVersions.V7, - lambda room_version: room_version.knock_join_rule, - ), - RoomVersionCapability( - "restricted", - RoomVersions.V9, - lambda room_version: room_version.restricted_join_rule, - ), - ) -} diff --git a/synapse/config/experimental.py b/synapse/config/experimental.py index 1bd70ead09a..9b6ff482cfd 100644 --- a/synapse/config/experimental.py +++ b/synapse/config/experimental.py @@ -385,9 +385,6 @@ def read_config( # MSC3814 (dehydrated devices with SSSS) self.msc3814_enabled: bool = experimental.get("msc3814_enabled", False) - # MSC3244 (room version capabilities) - self.msc3244_enabled: bool = experimental.get("msc3244_enabled", True) - # MSC3266 (room summary api) self.msc3266_enabled: bool = experimental.get("msc3266_enabled", False) diff --git a/synapse/rest/client/capabilities.py b/synapse/rest/client/capabilities.py index baff999ab06..705d74dee13 100644 --- a/synapse/rest/client/capabilities.py +++ b/synapse/rest/client/capabilities.py @@ -21,7 +21,7 @@ from http import HTTPStatus from typing import TYPE_CHECKING -from synapse.api.room_versions import KNOWN_ROOM_VERSIONS, MSC3244_CAPABILITIES +from synapse.api.room_versions import KNOWN_ROOM_VERSIONS from synapse.http.server import HttpServer from synapse.http.servlet import RestServlet from synapse.http.site import SynapseRequest @@ -77,11 +77,6 @@ async def on_GET(self, request: SynapseRequest) -> tuple[int, JsonDict]: } } - if self.config.experimental.msc3244_enabled: - response["capabilities"]["m.room_versions"][ - "org.matrix.msc3244.room_capabilities" - ] = MSC3244_CAPABILITIES - if self.config.experimental.msc3720_enabled: response["capabilities"]["org.matrix.msc3720.account_status"] = { "enabled": True, diff --git a/tests/rest/client/test_capabilities.py b/tests/rest/client/test_capabilities.py index 0eec3130618..2567fee2a4a 100644 --- a/tests/rest/client/test_capabilities.py +++ b/tests/rest/client/test_capabilities.py @@ -209,46 +209,6 @@ def test_get_change_3pid_capabilities_3pid_disabled(self) -> None: self.assertEqual(channel.code, HTTPStatus.OK) self.assertFalse(capabilities["m.3pid_changes"]["enabled"]) - @override_config({"experimental_features": {"msc3244_enabled": False}}) - def test_get_does_not_include_msc3244_fields_when_disabled(self) -> None: - access_token = self.get_success( - self.auth_handler.create_access_token_for_user_id( - self.user, device_id=None, valid_until_ms=None - ) - ) - - channel = self.make_request("GET", self.url, access_token=access_token) - capabilities = channel.json_body["capabilities"] - - self.assertEqual(channel.code, 200) - self.assertNotIn( - "org.matrix.msc3244.room_capabilities", capabilities["m.room_versions"] - ) - - def test_get_does_include_msc3244_fields_when_enabled(self) -> None: - access_token = self.get_success( - self.auth_handler.create_access_token_for_user_id( - self.user, device_id=None, valid_until_ms=None - ) - ) - - channel = self.make_request("GET", self.url, access_token=access_token) - capabilities = channel.json_body["capabilities"] - - self.assertEqual(channel.code, 200) - for details in capabilities["m.room_versions"][ - "org.matrix.msc3244.room_capabilities" - ].values(): - if details["preferred"] is not None: - self.assertTrue( - details["preferred"] in KNOWN_ROOM_VERSIONS, - str(details["preferred"]), - ) - - self.assertGreater(len(details["support"]), 0) - for room_version in details["support"]: - self.assertTrue(room_version in KNOWN_ROOM_VERSIONS, str(room_version)) - def test_get_get_token_login_fields_when_disabled(self) -> None: """By default login via an existing session is disabled.""" access_token = self.get_success( From 46d235cd525e2c9208e531a1b6c1adb6eae8cab8 Mon Sep 17 00:00:00 2001 From: Eric Eastwood Date: Thu, 5 Feb 2026 17:11:55 -0600 Subject: [PATCH 009/121] Add in-repo Complement tests (#19406) This is useful so we can test Synapse specific behaviors like our admin API. (see docs in PR, `complement/README.md`) ``` COMPLEMENT_DIR=../complement ./scripts-dev/complement.sh --in-repo ``` Complement calls these ["out-of-repo"](https://github.com/matrix-org/complement/blob/78c255edcebfcb0ac5e3c86d49d76cb21fdd035a/OUT-OF-REPO-TESTS.md) tests but it's a bit of a misnomer once they're in your project. (just depends on the perspective) There has been [previous desire](https://github.com/element-hq/synapse/pull/19021#discussion_r2453442191) for this kind of thing but this is spawning from wanting to have some tests for our purge history admin API (https://github.com/element-hq/synapse-rust-apps/issues/430). There are some Sytest tests ([`matrix-org/sytest` -> `tests/48admin.pl#L91-L618`](https://github.com/matrix-org/sytest/blob/1be04cce46c0f84abac736390dc3fab17f35a756/tests/48admin.pl#L91-L618)) for this already but I'd much rather work in Complement instead of Sytest. I'm wanting these tests to ensure that our new `event-cache` rust app for Synapse Pro doesn't break these kind of erasure features (https://github.com/element-hq/synapse-rust-apps/issues/366 and https://github.com/element-hq/synapse-rust-apps/issues/153). Interestingly, there is already [`matrix-org/complement` -> `tests/csapi/admin_test.go`](https://github.com/matrix-org/complement/blob/78c255edcebfcb0ac5e3c86d49d76cb21fdd035a/tests/csapi/admin_test.go) (added in https://github.com/matrix-org/complement/pull/322) in the Complement repo iteslf that tests the `/_synapse/admin/v1/send_server_notice` endpoint but it's a bit of an interesting case as [Dendrite also supports this endpoint](https://github.com/matrix-org/dendrite/pull/2180). I don't think it's good practice to continually shove in more and more Synapse-specific behavior into the Complement repo itself. We already have success with other out-of-repo tests for projects like the [SBG](https://github.com/element-hq/sbg/tree/b76b05b53e40bf6890e51dd1b83cec3460274eb2/complement_tests), [TI-Messenger Proxy](https://github.com/element-hq/ti-messenger-proxy/tree/c8fa87feccc743c01cccbbc2685321206b532925/complement), and our [Synapse Pro for small hosts](https://github.com/element-hq/synapse-small-hosts/tree/c2ea7eabf3e1d7c26a5312ebef326b254937be99/complement). --- .github/workflows/latest_deps.yml | 50 ++- .github/workflows/tests.yml | 30 +- .github/workflows/twisted_trunk.yml | 52 ++- changelog.d/19406.misc | 1 + complement/.golangci.yml | 38 ++ complement/README.md | 54 +++ complement/go.mod | 209 ++++++++++ complement/go.sum | 587 ++++++++++++++++++++++++++++ complement/tests/federation_test.go | 65 +++ complement/tests/main_test.go | 23 ++ scripts-dev/complement.sh | 45 ++- scripts-dev/lint.sh | 11 + 12 files changed, 1144 insertions(+), 21 deletions(-) create mode 100644 changelog.d/19406.misc create mode 100644 complement/.golangci.yml create mode 100644 complement/README.md create mode 100644 complement/go.mod create mode 100644 complement/go.sum create mode 100644 complement/tests/federation_test.go create mode 100644 complement/tests/main_test.go diff --git a/.github/workflows/latest_deps.yml b/.github/workflows/latest_deps.yml index 9e0f2c384e9..dda4e5f57bf 100644 --- a/.github/workflows/latest_deps.yml +++ b/.github/workflows/latest_deps.yml @@ -126,7 +126,6 @@ jobs: -exec echo "::endgroup::" \; || true - sytest: needs: check_repo if: needs.check_repo.outputs.should_run_workflow == 'true' @@ -181,7 +180,6 @@ jobs: /logs/results.tap /logs/**/*.log* - complement: needs: check_repo if: "!failure() && !cancelled() && needs.check_repo.outputs.should_run_workflow == 'true'" @@ -214,11 +212,53 @@ jobs: cache-dependency-path: complement/go.sum go-version-file: complement/go.mod - - run: | + - name: Run Complement Tests + id: run_complement_tests + # -p=1: We're using `-p 1` to force the test packages to run serially as GHA boxes + # are underpowered and don't like running tons of Synapse instances at once. + # -json: Output JSON format so that gotestfmt can parse it. + # + # tee /tmp/gotest-complement.log: We tee the output to a file so that we can re-process it + # later on for better formatting with gotestfmt. But we still want the command + # to output to the terminal as it runs so we can see what's happening in + # real-time. + run: | set -o pipefail - TEST_ONLY_IGNORE_POETRY_LOCKFILE=1 POSTGRES=${{ (matrix.database == 'Postgres') && 1 || '' }} WORKERS=${{ (matrix.arrangement == 'workers') && 1 || '' }} COMPLEMENT_DIR=`pwd`/complement synapse/scripts-dev/complement.sh -json 2>&1 | synapse/.ci/scripts/gotestfmt + COMPLEMENT_DIR=`pwd`/complement synapse/scripts-dev/complement.sh -p 1 -json 2>&1 | tee /tmp/gotest-complement.log shell: bash - name: Run Complement Tests + env: + POSTGRES: ${{ (matrix.database == 'Postgres') && 1 || '' }} + WORKERS: ${{ (matrix.arrangement == 'workers') && 1 || '' }} + TEST_ONLY_IGNORE_POETRY_LOCKFILE: 1 + + - name: Formatted Complement test logs + # Always run this step if we attempted to run the Complement tests. + if: always() && steps.run_complement_tests.outcome != 'skipped' + run: cat /tmp/gotest-complement.log | gotestfmt -hide "successful-downloads,empty-packages" + + - name: Run in-repo Complement Tests + id: run_in_repo_complement_tests + # -p=1: We're using `-p 1` to force the test packages to run serially as GHA boxes + # are underpowered and don't like running tons of Synapse instances at once. + # -json: Output JSON format so that gotestfmt can parse it. + # + # tee /tmp/gotest-in-repo-complement.log: We tee the output to a file so that we can re-process it + # later on for better formatting with gotestfmt. But we still want the command + # to output to the terminal as it runs so we can see what's happening in + # real-time. + run: | + set -o pipefail + COMPLEMENT_DIR=`pwd`/complement synapse/scripts-dev/complement.sh --in-repo -p 1 -json 2>&1 | tee /tmp/gotest-in-repo-complement.log + shell: bash + env: + POSTGRES: ${{ (matrix.database == 'Postgres') && 1 || '' }} + WORKERS: ${{ (matrix.arrangement == 'workers') && 1 || '' }} + TEST_ONLY_IGNORE_POETRY_LOCKFILE: 1 + + - name: Formatted in-repo Complement test logs + # Always run this step if we attempted to run the Complement tests. + if: always() && steps.run_in_repo_complement_tests.outcome != 'skipped' + run: cat /tmp/gotest-in-repo-complement.log | gotestfmt -hide "successful-downloads,empty-packages" # Open an issue if the build fails, so we know about it. # Only do this if we're not experimenting with this action in a PR. diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 715dfa93d9a..f575adb5c57 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -707,20 +707,19 @@ jobs: cache-dependency-path: complement/go.sum go-version-file: complement/go.mod - # use p=1 concurrency as GHA boxes are underpowered and don't like running tons of synapses at once. - name: Run Complement Tests id: run_complement_tests # -p=1: We're using `-p 1` to force the test packages to run serially as GHA boxes # are underpowered and don't like running tons of Synapse instances at once. # -json: Output JSON format so that gotestfmt can parse it. # - # tee /tmp/gotest.log: We tee the output to a file so that we can re-process it + # tee /tmp/gotest-complement.log: We tee the output to a file so that we can re-process it # later on for better formatting with gotestfmt. But we still want the command # to output to the terminal as it runs so we can see what's happening in # real-time. run: | set -o pipefail - COMPLEMENT_DIR=`pwd`/complement synapse/scripts-dev/complement.sh -p 1 -json 2>&1 | tee /tmp/gotest.log + COMPLEMENT_DIR=`pwd`/complement synapse/scripts-dev/complement.sh -p 1 -json 2>&1 | tee /tmp/gotest-complement.log shell: bash env: POSTGRES: ${{ (matrix.database == 'Postgres') && 1 || '' }} @@ -729,7 +728,30 @@ jobs: - name: Formatted Complement test logs # Always run this step if we attempted to run the Complement tests. if: always() && steps.run_complement_tests.outcome != 'skipped' - run: cat /tmp/gotest.log | gotestfmt -hide "successful-downloads,empty-packages" + run: cat /tmp/gotest-complement.log | gotestfmt -hide "successful-downloads,empty-packages" + + - name: Run in-repo Complement Tests + id: run_in_repo_complement_tests + # -p=1: We're using `-p 1` to force the test packages to run serially as GHA boxes + # are underpowered and don't like running tons of Synapse instances at once. + # -json: Output JSON format so that gotestfmt can parse it. + # + # tee /tmp/gotest-in-repo-complement.log: We tee the output to a file so that we can re-process it + # later on for better formatting with gotestfmt. But we still want the command + # to output to the terminal as it runs so we can see what's happening in + # real-time. + run: | + set -o pipefail + COMPLEMENT_DIR=`pwd`/complement synapse/scripts-dev/complement.sh --in-repo -p 1 -json 2>&1 | tee /tmp/gotest-in-repo-complement.log + shell: bash + env: + POSTGRES: ${{ (matrix.database == 'Postgres') && 1 || '' }} + WORKERS: ${{ (matrix.arrangement == 'workers') && 1 || '' }} + + - name: Formatted in-repo Complement test logs + # Always run this step if we attempted to run the Complement tests. + if: always() && steps.run_in_repo_complement_tests.outcome != 'skipped' + run: cat /tmp/gotest-in-repo-complement.log | gotestfmt -hide "successful-downloads,empty-packages" cargo-test: if: ${{ needs.changes.outputs.rust == 'true' }} diff --git a/.github/workflows/twisted_trunk.yml b/.github/workflows/twisted_trunk.yml index bd5c79f16dc..12fdbbe7c47 100644 --- a/.github/workflows/twisted_trunk.yml +++ b/.github/workflows/twisted_trunk.yml @@ -12,10 +12,9 @@ on: twisted_ref: description: Commit, branch or tag to checkout from upstream Twisted. required: false - default: 'trunk' + default: "trunk" type: string - concurrency: group: ${{ github.workflow }}-${{ github.ref }} cancel-in-progress: true @@ -89,7 +88,6 @@ jobs: poetry add --extras tls git+https://github.com/twisted/twisted.git#trunk poetry install --no-interaction --extras "all test" - run: poetry run trial --jobs 2 tests - - name: Dump logs # Logs are most useful when the command fails, always include them. if: ${{ always() }} @@ -199,11 +197,53 @@ jobs: poetry lock working-directory: synapse - - run: | + - name: Run Complement Tests + id: run_complement_tests + # -p=1: We're using `-p 1` to force the test packages to run serially as GHA boxes + # are underpowered and don't like running tons of Synapse instances at once. + # -json: Output JSON format so that gotestfmt can parse it. + # + # tee /tmp/gotest-complement.log: We tee the output to a file so that we can re-process it + # later on for better formatting with gotestfmt. But we still want the command + # to output to the terminal as it runs so we can see what's happening in + # real-time. + run: | set -o pipefail - TEST_ONLY_SKIP_DEP_HASH_VERIFICATION=1 POSTGRES=${{ (matrix.database == 'Postgres') && 1 || '' }} WORKERS=${{ (matrix.arrangement == 'workers') && 1 || '' }} COMPLEMENT_DIR=`pwd`/complement synapse/scripts-dev/complement.sh -json 2>&1 | synapse/.ci/scripts/gotestfmt + COMPLEMENT_DIR=`pwd`/complement synapse/scripts-dev/complement.sh -p 1 -json 2>&1 | tee /tmp/gotest-complement.log shell: bash - name: Run Complement Tests + env: + POSTGRES: ${{ (matrix.database == 'Postgres') && 1 || '' }} + WORKERS: ${{ (matrix.arrangement == 'workers') && 1 || '' }} + TEST_ONLY_SKIP_DEP_HASH_VERIFICATION: 1 + + - name: Formatted Complement test logs + # Always run this step if we attempted to run the Complement tests. + if: always() && steps.run_complement_tests.outcome != 'skipped' + run: cat /tmp/gotest-complement.log | gotestfmt -hide "successful-downloads,empty-packages" + + - name: Run in-repo Complement Tests + id: run_in_repo_complement_tests + # -p=1: We're using `-p 1` to force the test packages to run serially as GHA boxes + # are underpowered and don't like running tons of Synapse instances at once. + # -json: Output JSON format so that gotestfmt can parse it. + # + # tee /tmp/gotest-in-repo-complement.log: We tee the output to a file so that we can re-process it + # later on for better formatting with gotestfmt. But we still want the command + # to output to the terminal as it runs so we can see what's happening in + # real-time. + run: | + set -o pipefail + COMPLEMENT_DIR=`pwd`/complement synapse/scripts-dev/complement.sh --in-repo -p 1 -json 2>&1 | tee /tmp/gotest-in-repo-complement.log + shell: bash + env: + POSTGRES: ${{ (matrix.database == 'Postgres') && 1 || '' }} + WORKERS: ${{ (matrix.arrangement == 'workers') && 1 || '' }} + TEST_ONLY_SKIP_DEP_HASH_VERIFICATION: 1 + + - name: Formatted in-repo Complement test logs + # Always run this step if we attempted to run the Complement tests. + if: always() && steps.run_in_repo_complement_tests.outcome != 'skipped' + run: cat /tmp/gotest-in-repo-complement.log | gotestfmt -hide "successful-downloads,empty-packages" # open an issue if the build fails, so we know about it. open-issue: diff --git a/changelog.d/19406.misc b/changelog.d/19406.misc new file mode 100644 index 00000000000..bfe8d6afb0f --- /dev/null +++ b/changelog.d/19406.misc @@ -0,0 +1 @@ +Add in-repo Complement tests so we can test Synapse specific behavior at an end-to-end level. diff --git a/complement/.golangci.yml b/complement/.golangci.yml new file mode 100644 index 00000000000..6ef564d4c44 --- /dev/null +++ b/complement/.golangci.yml @@ -0,0 +1,38 @@ +# Docs: https://golangci-lint.run/docs/configuration/file/ +# +# Go formatting/linting rules +# +# This is run as part of the normal linting utility script, +# `poetry run ./scripts-dev/lint.sh` + +version: "2" + +linters: + # Default set of linters. + # The value can be: + # - `standard`: https://golangci-lint.run/docs/linters/#enabled-by-default + # - `all`: enables all linters by default. + # - `none`: disables all linters by default. + # - `fast`: enables only linters considered as "fast" (`golangci-lint help linters --json | jq '[ .[] | select(.fast==true) ] | map(.name)'`). + # Default: standard + default: standard + + # Enable specific linter. + # enable: + # - example + + # Disable specific linters. + disable: + # FIXME: Ideally, we'd enable the `bodyclose` lint but there are many + # false-positives (like https://github.com/timakin/bodyclose/issues/39) and just is + # not well-suited for our use case (https://github.com/timakin/bodyclose/issues/11 and + # https://github.com/timakin/bodyclose/issues/76). + - bodyclose + +formatters: + # Enable specific formatter. + # Default: [] (uses standard Go formatting) + enable: + - gofmt + - goimports + - golines diff --git a/complement/README.md b/complement/README.md new file mode 100644 index 00000000000..c4a95bdd63a --- /dev/null +++ b/complement/README.md @@ -0,0 +1,54 @@ +# Complement testing + +Complement is a black box integration testing framework for Matrix homeservers. It +allows us to write end-to-end tests that interact with real Synapse homeservers to +ensure everything works at a holistic level. + + +## Setup + +Nothing beyond a [normal Complement +setup](https://github.com/matrix-org/complement?tab=readme-ov-file#running) (just Go and +Docker). + + +## Running tests + +Run tests from the [Complement](https://github.com/matrix-org/complement) repo: + +```shell +# Run the tests +./scripts-dev/complement.sh + +# To run a whole group of tests, you can specify part of the test path: +scripts-dev/complement.sh ./tests/csapi/... -run TestRoomCreate +# To run a specific test, you can specify the whole name structure: +scripts-dev/complement.sh ./tests/csapi/... -run TestRoomCreate/Parallel/POST_/createRoom_makes_a_public_room +# Generally though, the `-run` parameter accepts regex patterns, so you can match however you like: +scripts-dev/complement.sh ./tests/... -run 'TestRoomCreate/Parallel/POST_/createRoom_makes_a_(.*)' +``` + +Typically, if you're developing the Synapse and Complement tests side-by-side, you will +run something like this: + +```shell +# To run a specific test +COMPLEMENT_DIR=../complement ./scripts-dev/complement.sh ./tests/csapi/... -run TestRoomCreate +``` + + +### Running in-repo tests + +In-repo Complement tests are tests that are vendored into this project. We use the +in-repo test suite to test Synapse specific behaviors like the admin API. + +To run the in-repo Complement tests, use the `--in-repo` command line argument. + +```shell +# Run only a specific test package. +# Note: test packages are relative to the `./complement` directory in this project +./scripts-dev/complement.sh --in-repo ./tests/... + +# Similarly, you can also use `-run` to specify all or part of a specific test path to run +scripts-dev/complement.sh --in-repo ./tests/... -run TestIntraShardFederation +``` diff --git a/complement/go.mod b/complement/go.mod new file mode 100644 index 00000000000..062ea8e6ee9 --- /dev/null +++ b/complement/go.mod @@ -0,0 +1,209 @@ +module github.com/element-hq/synapse + +go 1.24.1 + +toolchain go1.24.4 + +require ( + github.com/docker/docker v28.2.2+incompatible + github.com/docker/go-connections v0.5.0 + github.com/matrix-org/complement v0.0.0-20251120181401-44111a2a8a9d + github.com/matrix-org/gomatrixserverlib v0.0.0-20250813150445-9f5070a65744 + github.com/minio/operator v0.0.0-20250423184541-6eee6a7caa70 + github.com/mittwald/go-helm-client v0.12.18 + gopkg.in/yaml.v2 v2.4.0 + k8s.io/api v0.33.2 + k8s.io/apimachinery v0.33.2 + k8s.io/client-go v0.33.2 + sigs.k8s.io/controller-runtime v0.21.0 +) + +require ( + github.com/cloudnative-pg/barman-cloud v0.3.1 // indirect + github.com/cloudnative-pg/machinery v0.2.0 // indirect + github.com/dustin/go-humanize v1.0.1 // indirect + github.com/evanphx/json-patch/v5 v5.9.11 // indirect + github.com/fsnotify/fsnotify v1.8.0 // indirect + github.com/go-ini/ini v1.67.0 // indirect + github.com/go-logr/zapr v1.3.0 // indirect + github.com/go-ole/go-ole v1.3.0 // indirect + github.com/goccy/go-json v0.10.5 // indirect + github.com/golang-jwt/jwt/v4 v4.5.2 // indirect + github.com/golang/protobuf v1.5.4 // indirect + github.com/hashicorp/go-set/v3 v3.0.0 // indirect + github.com/klauspost/cpuid/v2 v2.2.10 // indirect + github.com/kubernetes-csi/external-snapshotter/client/v8 v8.2.0 // indirect + github.com/lufia/plan9stats v0.0.0-20250317134145-8bc96cf8fc35 // indirect + github.com/matrix-org/gomatrix v0.0.0-20220926102614-ceba4d9f7530 // indirect + github.com/matttproud/golang_protobuf_extensions v1.0.4 // indirect + github.com/miekg/dns v1.1.66 // indirect + github.com/minio/crc64nvme v1.0.1 // indirect + github.com/minio/madmin-go/v3 v3.0.100 // indirect + github.com/minio/md5-simd v1.1.2 // indirect + github.com/minio/minio-go/v7 v7.0.89 // indirect + github.com/moby/sys/atomicwriter v0.1.0 // indirect + github.com/oleiade/lane/v2 v2.0.0 // indirect + github.com/philhofer/fwd v1.1.3-0.20240916144458-20a13a1f6b7c // indirect + github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 // indirect + github.com/prometheus-operator/prometheus-operator/pkg/apis/monitoring v0.80.1 // indirect + github.com/prometheus/prom2json v1.4.1 // indirect + github.com/prometheus/prometheus v0.302.1 // indirect + github.com/rs/xid v1.6.0 // indirect + github.com/safchain/ethtool v0.5.10 // indirect + github.com/secure-io/sio-go v0.3.1 // indirect + github.com/shirou/gopsutil/v3 v3.24.5 // indirect + github.com/shoenig/go-m1cpu v0.1.6 // indirect + github.com/tinylib/msgp v1.2.5 // indirect + github.com/tklauser/go-sysconf v0.3.15 // indirect + github.com/tklauser/numcpus v0.10.0 // indirect + github.com/yusufpapurcu/wmi v1.2.4 // indirect + go.uber.org/multierr v1.11.0 // indirect + go.uber.org/zap v1.27.0 // indirect + golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 // indirect + golang.org/x/mod v0.29.0 // indirect + golang.org/x/tools v0.38.0 // indirect + gomodules.xyz/jsonpatch/v2 v2.4.0 // indirect + gotest.tools/v3 v3.4.0 // indirect + oras.land/oras-go/v2 v2.6.0 // indirect +) + +require ( + dario.cat/mergo v1.0.1 // indirect + github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c // indirect + github.com/BurntSushi/toml v1.5.0 // indirect + github.com/MakeNowJust/heredoc v1.0.0 // indirect + github.com/Masterminds/goutils v1.1.1 // indirect + github.com/Masterminds/semver/v3 v3.3.1 // indirect + github.com/Masterminds/sprig/v3 v3.3.0 // indirect + github.com/Masterminds/squirrel v1.5.4 // indirect + github.com/Microsoft/go-winio v0.6.2 // indirect + github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 // indirect + github.com/beorn7/perks v1.0.1 // indirect + github.com/blang/semver/v4 v4.0.0 // indirect + github.com/cespare/xxhash/v2 v2.3.0 // indirect + github.com/chai2010/gettext-go v1.0.2 // indirect + github.com/cloudnative-pg/cloudnative-pg v1.26.0 + github.com/containerd/containerd v1.7.27 // indirect + github.com/containerd/errdefs v1.0.0 // indirect + github.com/containerd/errdefs/pkg v0.3.0 // indirect + github.com/containerd/log v0.1.0 // indirect + github.com/containerd/platforms v0.2.1 // indirect + github.com/cyphar/filepath-securejoin v0.4.1 // indirect + github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect + github.com/distribution/reference v0.6.0 // indirect + github.com/docker/go-units v0.5.0 // indirect + github.com/emicklei/go-restful/v3 v3.12.2 // indirect + github.com/evanphx/json-patch v5.9.11+incompatible // indirect + github.com/exponent-io/jsonpath v0.0.0-20210407135951-1de76d718b3f // indirect + github.com/fatih/color v1.18.0 // indirect + github.com/felixge/httpsnoop v1.0.4 // indirect + github.com/fxamacker/cbor/v2 v2.7.0 // indirect + github.com/go-errors/errors v1.5.1 // indirect + github.com/go-gorp/gorp/v3 v3.1.0 // indirect + github.com/go-logr/logr v1.4.3 // indirect + github.com/go-logr/stdr v1.2.2 // indirect + github.com/go-openapi/jsonpointer v0.21.1 // indirect + github.com/go-openapi/jsonreference v0.21.0 // indirect + github.com/go-openapi/swag v0.23.1 // indirect + github.com/gobwas/glob v0.2.3 // indirect + github.com/gogo/protobuf v1.3.2 // indirect + github.com/google/btree v1.1.3 // indirect + github.com/google/gnostic-models v0.6.9 // indirect + github.com/google/go-cmp v0.7.0 // indirect + github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 // indirect + github.com/google/uuid v1.6.0 // indirect + github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674 // indirect + github.com/gosuri/uitable v0.0.4 // indirect + github.com/gregjones/httpcache v0.0.0-20190611155906-901d90724c79 // indirect + github.com/hashicorp/errwrap v1.1.0 // indirect + github.com/hashicorp/go-multierror v1.1.1 // indirect + github.com/huandu/xstrings v1.5.0 // indirect + github.com/inconshreveable/mousetrap v1.1.0 // indirect + github.com/jmoiron/sqlx v1.4.0 // indirect + github.com/josharian/intern v1.0.0 // indirect + github.com/json-iterator/go v1.1.12 // indirect + github.com/klauspost/compress v1.18.0 // indirect + github.com/lann/builder v0.0.0-20180802200727-47ae307949d0 // indirect + github.com/lann/ps v0.0.0-20150810152359-62de8c46ede0 // indirect + github.com/lib/pq v1.10.9 // indirect + github.com/liggitt/tabwriter v0.0.0-20181228230101-89fcab3d43de // indirect + github.com/mailru/easyjson v0.9.0 // indirect + github.com/matrix-org/util v0.0.0-20221111132719-399730281e66 // indirect + github.com/mattn/go-colorable v0.1.14 // indirect + github.com/mattn/go-isatty v0.0.20 // indirect + github.com/mattn/go-runewidth v0.0.15 // indirect + github.com/mitchellh/copystructure v1.2.0 // indirect + github.com/mitchellh/go-wordwrap v1.0.1 // indirect + github.com/mitchellh/reflectwalk v1.0.2 // indirect + github.com/moby/docker-image-spec v1.3.1 // indirect + github.com/moby/spdystream v0.5.0 // indirect + github.com/moby/term v0.5.2 // indirect + github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect + github.com/modern-go/reflect2 v1.0.2 // indirect + github.com/monochromegane/go-gitignore v0.0.0-20200626010858-205db1a8cc00 // indirect + github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect + github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f // indirect + github.com/opencontainers/go-digest v1.0.0 // indirect + github.com/opencontainers/image-spec v1.1.1 // indirect + github.com/peterbourgon/diskv v2.0.1+incompatible // indirect + github.com/pkg/errors v0.9.1 // indirect + github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect + github.com/prometheus/client_golang v1.22.0 // indirect + github.com/prometheus/client_model v0.6.2 + github.com/prometheus/common v0.65.0 + github.com/prometheus/procfs v0.16.0 // indirect + github.com/rivo/uniseg v0.4.4 // indirect + github.com/rubenv/sql-migrate v1.8.0 // indirect + github.com/russross/blackfriday/v2 v2.1.0 // indirect + github.com/shopspring/decimal v1.4.0 // indirect + github.com/sirupsen/logrus v1.9.3 // indirect + github.com/spf13/cast v1.7.0 // indirect + github.com/spf13/cobra v1.9.1 // indirect + github.com/spf13/pflag v1.0.6 // indirect + github.com/stretchr/testify v1.10.0 + github.com/tidwall/gjson v1.18.0 // indirect + github.com/tidwall/match v1.1.1 // indirect + github.com/tidwall/pretty v1.2.1 // indirect + github.com/tidwall/sjson v1.2.5 // indirect + github.com/x448/float16 v0.8.4 // indirect + github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb // indirect + github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect + github.com/xeipuuv/gojsonschema v1.2.0 // indirect + github.com/xlab/treeprint v1.2.0 // indirect + go.opentelemetry.io/auto/sdk v1.1.0 // indirect + go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0 // indirect + go.opentelemetry.io/otel v1.36.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.36.0 // indirect + go.opentelemetry.io/otel/metric v1.36.0 // indirect + go.opentelemetry.io/otel/trace v1.36.0 // indirect + go.opentelemetry.io/proto/otlp v1.7.0 // indirect + golang.org/x/crypto v0.45.0 // indirect + golang.org/x/net v0.47.0 // indirect + golang.org/x/oauth2 v0.30.0 // indirect + golang.org/x/sync v0.18.0 // indirect + golang.org/x/sys v0.38.0 // indirect + golang.org/x/term v0.37.0 // indirect + golang.org/x/text v0.31.0 // indirect + golang.org/x/time v0.11.0 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20250528174236-200df99c418a // indirect + google.golang.org/grpc v1.72.2 // indirect + google.golang.org/protobuf v1.36.6 // indirect + gopkg.in/evanphx/json-patch.v4 v4.12.0 // indirect + gopkg.in/inf.v0 v0.9.1 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect + helm.sh/helm/v3 v3.18.4 // indirect + k8s.io/apiextensions-apiserver v0.33.2 // indirect + k8s.io/apiserver v0.33.2 // indirect + k8s.io/cli-runtime v0.33.2 // indirect + k8s.io/component-base v0.33.2 // indirect + k8s.io/klog/v2 v2.130.1 + k8s.io/kube-openapi v0.0.0-20250318190949-c8a335a9a2ff // indirect + k8s.io/kubectl v0.33.2 // indirect + k8s.io/utils v0.0.0-20250502105355-0f33e8f1c979 // indirect + sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8 // indirect + sigs.k8s.io/kustomize/api v0.19.0 // indirect + sigs.k8s.io/kustomize/kyaml v0.19.0 // indirect + sigs.k8s.io/randfill v1.0.0 // indirect + sigs.k8s.io/structured-merge-diff/v4 v4.6.0 // indirect + sigs.k8s.io/yaml v1.4.0 // indirect +) diff --git a/complement/go.sum b/complement/go.sum new file mode 100644 index 00000000000..2ff86089531 --- /dev/null +++ b/complement/go.sum @@ -0,0 +1,587 @@ +dario.cat/mergo v1.0.1 h1:Ra4+bf83h2ztPIQYNP99R6m+Y7KfnARDfID+a+vLl4s= +dario.cat/mergo v1.0.1/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk= +filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA= +filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4= +github.com/AdaLogics/go-fuzz-headers v0.0.0-20230811130428-ced1acdcaa24 h1:bvDV9vkmnHYOMsOr4WLk+Vo07yKIzd94sVoIqshQ4bU= +github.com/AdaLogics/go-fuzz-headers v0.0.0-20230811130428-ced1acdcaa24/go.mod h1:8o94RPi1/7XTJvwPpRSzSUedZrtlirdB3r9Z20bi2f8= +github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c h1:udKWzYgxTojEKWjV8V+WSxDXJ4NFATAsZjh8iIbsQIg= +github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E= +github.com/BurntSushi/toml v1.5.0 h1:W5quZX/G/csjUnuI8SUYlsHs9M38FC7znL0lIO+DvMg= +github.com/BurntSushi/toml v1.5.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho= +github.com/DATA-DOG/go-sqlmock v1.5.2 h1:OcvFkGmslmlZibjAjaHm3L//6LiuBgolP7OputlJIzU= +github.com/DATA-DOG/go-sqlmock v1.5.2/go.mod h1:88MAG/4G7SMwSE3CeA0ZKzrT5CiOU3OJ+JlNzwDqpNU= +github.com/MakeNowJust/heredoc v1.0.0 h1:cXCdzVdstXyiTqTvfqk9SDHpKNjxuom+DOlyEeQ4pzQ= +github.com/MakeNowJust/heredoc v1.0.0/go.mod h1:mG5amYoWBHf8vpLOuehzbGGw0EHxpZZ6lCpQ4fNJ8LE= +github.com/Masterminds/goutils v1.1.1 h1:5nUrii3FMTL5diU80unEVvNevw1nH4+ZV4DSLVJLSYI= +github.com/Masterminds/goutils v1.1.1/go.mod h1:8cTjp+g8YejhMuvIA5y2vz3BpJxksy863GQaJW2MFNU= +github.com/Masterminds/semver/v3 v3.3.1 h1:QtNSWtVZ3nBfk8mAOu/B6v7FMJ+NHTIgUPi7rj+4nv4= +github.com/Masterminds/semver/v3 v3.3.1/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM= +github.com/Masterminds/sprig/v3 v3.3.0 h1:mQh0Yrg1XPo6vjYXgtf5OtijNAKJRNcTdOOGZe3tPhs= +github.com/Masterminds/sprig/v3 v3.3.0/go.mod h1:Zy1iXRYNqNLUolqCpL4uhk6SHUMAOSCzdgBfDb35Lz0= +github.com/Masterminds/squirrel v1.5.4 h1:uUcX/aBc8O7Fg9kaISIUsHXdKuqehiXAMQTYX8afzqM= +github.com/Masterminds/squirrel v1.5.4/go.mod h1:NNaOrjSoIDfDA40n7sr2tPNZRfjzjA400rg+riTZj10= +github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY= +github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU= +github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio= +github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs= +github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 h1:DklsrG3dyBCFEj5IhUbnKptjxatkF07cF2ak3yi77so= +github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2/go.mod h1:WaHUgvxTVq04UNunO+XhnAqY/wQc+bxr74GqbsZ/Jqw= +github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= +github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= +github.com/blang/semver/v4 v4.0.0 h1:1PFHFE6yCCTv8C1TeyNNarDzntLi7wMI5i/pzqYIsAM= +github.com/blang/semver/v4 v4.0.0/go.mod h1:IbckMUScFkM3pff0VJDNKRiT6TG/YpiHIM2yvyW5YoQ= +github.com/bshuster-repo/logrus-logstash-hook v1.0.0 h1:e+C0SB5R1pu//O4MQ3f9cFuPGoOVeF2fE4Og9otCc70= +github.com/bshuster-repo/logrus-logstash-hook v1.0.0/go.mod h1:zsTqEiSzDgAa/8GZR7E1qaXrhYNDKBYy5/dWPTIflbk= +github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8= +github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= +github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= +github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/chai2010/gettext-go v1.0.2 h1:1Lwwip6Q2QGsAdl/ZKPCwTe9fe0CjlUbqj5bFNSjIRk= +github.com/chai2010/gettext-go v1.0.2/go.mod h1:y+wnP2cHYaVj19NZhYKAwEMH2CI1gNHeQQ+5AjwawxA= +github.com/cloudnative-pg/barman-cloud v0.3.1 h1:kzkY77k2lN/caoyh7ibXDSZjJeSJTNvnVt6Gfa8Iq5M= +github.com/cloudnative-pg/barman-cloud v0.3.1/go.mod h1:4HL3AjY9oEl2Ed0HSkyvTZEQPhwyFOaAnuCz9lfVeYQ= +github.com/cloudnative-pg/cloudnative-pg v1.26.0 h1:X+ayWg6TXqglziSPejKiviPTf/CSh4AmYXx1mrS3i8s= +github.com/cloudnative-pg/cloudnative-pg v1.26.0/go.mod h1:aNh0g7UHKxyka4HYOqLaYDWtZ6tORQ7HbIHLvb79sx8= +github.com/cloudnative-pg/machinery v0.2.0 h1:x8OAwxdeL/6wkbxqorz+nX6UovTyx7/TBeCfiRebR2o= +github.com/cloudnative-pg/machinery v0.2.0/go.mod h1:Kg8W8Tb/1UFGGtw3hR8S5SytSWddlHaCnJSgBo4x/nc= +github.com/containerd/containerd v1.7.27 h1:yFyEyojddO3MIGVER2xJLWoCIn+Up4GaHFquP7hsFII= +github.com/containerd/containerd v1.7.27/go.mod h1:xZmPnl75Vc+BLGt4MIfu6bp+fy03gdHAn9bz+FreFR0= +github.com/containerd/errdefs v1.0.0 h1:tg5yIfIlQIrxYtu9ajqY42W3lpS19XqdxRQeEwYG8PI= +github.com/containerd/errdefs v1.0.0/go.mod h1:+YBYIdtsnF4Iw6nWZhJcqGSg/dwvV7tyJ/kCkyJ2k+M= +github.com/containerd/errdefs/pkg v0.3.0 h1:9IKJ06FvyNlexW690DXuQNx2KA2cUJXx151Xdx3ZPPE= +github.com/containerd/errdefs/pkg v0.3.0/go.mod h1:NJw6s9HwNuRhnjJhM7pylWwMyAkmCQvQ4GpJHEqRLVk= +github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I= +github.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo= +github.com/containerd/platforms v0.2.1 h1:zvwtM3rz2YHPQsF2CHYM8+KtB5dvhISiXh5ZpSBQv6A= +github.com/containerd/platforms v0.2.1/go.mod h1:XHCb+2/hzowdiut9rkudds9bE5yJ7npe7dG/wG+uFPw= +github.com/coreos/go-systemd/v22 v22.5.0 h1:RrqgGjYQKalulkV8NGVIfkXQf6YYmOyiJKk8iXXhfZs= +github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= +github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= +github.com/creack/pty v1.1.18 h1:n56/Zwd5o6whRC5PMGretI4IdRLlmBXYNjScPaBgsbY= +github.com/creack/pty v1.1.18/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4= +github.com/cyphar/filepath-securejoin v0.4.1 h1:JyxxyPEaktOD+GAnqIqTf9A8tHyAG22rowi7HkoSU1s= +github.com/cyphar/filepath-securejoin v0.4.1/go.mod h1:Sdj7gXlvMcPZsbhwhQ33GguGLDGQL7h7bg04C/+u9jI= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78= +github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc= +github.com/distribution/distribution/v3 v3.0.0 h1:q4R8wemdRQDClzoNNStftB2ZAfqOiN6UX90KJc4HjyM= +github.com/distribution/distribution/v3 v3.0.0/go.mod h1:tRNuFoZsUdyRVegq8xGNeds4KLjwLCRin/tTo6i1DhU= +github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk= +github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E= +github.com/docker/docker v28.2.2+incompatible h1:CjwRSksz8Yo4+RmQ339Dp/D2tGO5JxwYeqtMOEe0LDw= +github.com/docker/docker v28.2.2+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= +github.com/docker/docker-credential-helpers v0.9.3 h1:gAm/VtF9wgqJMoxzT3Gj5p4AqIjCBS4wrsOh9yRqcz8= +github.com/docker/docker-credential-helpers v0.9.3/go.mod h1:x+4Gbw9aGmChi3qTLZj8Dfn0TD20M/fuWy0E5+WDeCo= +github.com/docker/go-connections v0.5.0 h1:USnMq7hx7gwdVZq1L49hLXaFtUdTADjXGp+uj1Br63c= +github.com/docker/go-connections v0.5.0/go.mod h1:ov60Kzw0kKElRwhNs9UlUHAE/F9Fe6GLaXnqyDdmEXc= +github.com/docker/go-events v0.0.0-20190806004212-e31b211e4f1c h1:+pKlWGMw7gf6bQ+oDZB4KHQFypsfjYlq/C4rfL7D3g8= +github.com/docker/go-events v0.0.0-20190806004212-e31b211e4f1c/go.mod h1:Uw6UezgYA44ePAFQYUehOuCzmy5zmg/+nl2ZfMWGkpA= +github.com/docker/go-metrics v0.0.1 h1:AgB/0SvBxihN0X8OR4SjsblXkbMvalQ8cjmtKQ2rQV8= +github.com/docker/go-metrics v0.0.1/go.mod h1:cG1hvH2utMXtqgqqYE9plW6lDxS3/5ayHzueweSI3Vw= +github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4= +github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= +github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= +github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= +github.com/emicklei/go-restful/v3 v3.12.2 h1:DhwDP0vY3k8ZzE0RunuJy8GhNpPL6zqLkDf9B/a0/xU= +github.com/emicklei/go-restful/v3 v3.12.2/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= +github.com/evanphx/json-patch v5.9.11+incompatible h1:ixHHqfcGvxhWkniF1tWxBHA0yb4Z+d1UQi45df52xW8= +github.com/evanphx/json-patch v5.9.11+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk= +github.com/evanphx/json-patch/v5 v5.9.11 h1:/8HVnzMq13/3x9TPvjG08wUGqBTmZBsCWzjTM0wiaDU= +github.com/evanphx/json-patch/v5 v5.9.11/go.mod h1:3j+LviiESTElxA4p3EMKAB9HXj3/XEtnUf6OZxqIQTM= +github.com/exponent-io/jsonpath v0.0.0-20210407135951-1de76d718b3f h1:Wl78ApPPB2Wvf/TIe2xdyJxTlb6obmF18d8QdkxNDu4= +github.com/exponent-io/jsonpath v0.0.0-20210407135951-1de76d718b3f/go.mod h1:OSYXu++VVOHnXeitef/D8n/6y4QV8uLHSFXX4NeXMGc= +github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM= +github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU= +github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= +github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= +github.com/foxcpp/go-mockdns v1.1.0 h1:jI0rD8M0wuYAxL7r/ynTrCQQq0BVqfB99Vgk7DlmewI= +github.com/foxcpp/go-mockdns v1.1.0/go.mod h1:IhLeSFGed3mJIAXPH2aiRQB+kqz7oqu8ld2qVbOu7Wk= +github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= +github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= +github.com/fsnotify/fsnotify v1.8.0 h1:dAwr6QBTBZIkG8roQaJjGof0pp0EeF+tNV7YBP3F/8M= +github.com/fsnotify/fsnotify v1.8.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0= +github.com/fxamacker/cbor/v2 v2.7.0 h1:iM5WgngdRBanHcxugY4JySA0nk1wZorNOpTgCMedv5E= +github.com/fxamacker/cbor/v2 v2.7.0/go.mod h1:pxXPTn3joSm21Gbwsv0w9OSA2y1HFR9qXEeXQVeNoDQ= +github.com/go-errors/errors v1.5.1 h1:ZwEMSLRCapFLflTpT7NKaAc7ukJ8ZPEjzlxt8rPN8bk= +github.com/go-errors/errors v1.5.1/go.mod h1:sIVyrIiJhuEF+Pj9Ebtd6P/rEYROXFi3BopGUQ5a5Og= +github.com/go-gorp/gorp/v3 v3.1.0 h1:ItKF/Vbuj31dmV4jxA1qblpSwkl9g1typ24xoe70IGs= +github.com/go-gorp/gorp/v3 v3.1.0/go.mod h1:dLEjIyyRNiXvNZ8PSmzpt1GsWAUK8kjVhEpjH8TixEw= +github.com/go-ini/ini v1.67.0 h1:z6ZrTEZqSWOTyH2FlglNbNgARyHG8oLW9gMELqKr06A= +github.com/go-ini/ini v1.67.0/go.mod h1:ByCAeIL28uOIIG0E3PJtZPDL8WnHpFKFOtgjp+3Ies8= +github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= +github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= +github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= +github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= +github.com/go-logr/zapr v1.3.0 h1:XGdV8XW8zdwFiwOA2Dryh1gj2KRQyOOoNmBy4EplIcQ= +github.com/go-logr/zapr v1.3.0/go.mod h1:YKepepNBd1u/oyhd/yQmtjVXmm9uML4IXUgMOwR8/Gg= +github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0= +github.com/go-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE= +github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78= +github.com/go-openapi/jsonpointer v0.21.1 h1:whnzv/pNXtK2FbX/W9yJfRmE2gsmkfahjMKB0fZvcic= +github.com/go-openapi/jsonpointer v0.21.1/go.mod h1:50I1STOfbY1ycR8jGz8DaMeLCdXiI6aDteEdRNNzpdk= +github.com/go-openapi/jsonreference v0.21.0 h1:Rs+Y7hSXT83Jacb7kFyjn4ijOuVGSvOdF2+tg1TRrwQ= +github.com/go-openapi/jsonreference v0.21.0/go.mod h1:LmZmgsrTkVg9LG4EaHeY8cBDslNPMo06cago5JNLkm4= +github.com/go-openapi/swag v0.23.1 h1:lpsStH0n2ittzTnbaSloVZLuB5+fvSY/+hnagBjSNZU= +github.com/go-openapi/swag v0.23.1/go.mod h1:STZs8TbRvEQQKUA+JZNAm3EWlgaOBGpyFDqQnDHMef0= +github.com/go-sql-driver/mysql v1.8.1 h1:LedoTUt/eveggdHS9qUFC1EFSa8bU2+1pZjSRpvNJ1Y= +github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg= +github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI= +github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8= +github.com/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y= +github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8= +github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4= +github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M= +github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= +github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= +github.com/golang-jwt/jwt/v4 v4.5.2 h1:YtQM7lnr8iZ+j5q71MGKkNw9Mn7AjHM68uc9g5fXeUI= +github.com/golang-jwt/jwt/v4 v4.5.2/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0= +github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= +github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= +github.com/google/btree v1.1.3 h1:CVpQJjYgC4VbzxeGVHfvZrv1ctoYCAI8vbl07Fcxlyg= +github.com/google/btree v1.1.3/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl761Gm4= +github.com/google/gnostic-models v0.6.9 h1:MU/8wDLif2qCXZmzncUQ/BOfxWfthHi63KqpoNbWqVw= +github.com/google/gnostic-models v0.6.9/go.mod h1:CiWsm0s6BSQd1hRn8/QmxqB6BesYcbSZxsz9b0KuDBw= +github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= +github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= +github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0= +github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/pprof v0.0.0-20250403155104-27863c87afa6 h1:BHT72Gu3keYf3ZEu2J0b1vyeLSOYI8bm5wbJM/8yDe8= +github.com/google/pprof v0.0.0-20250403155104-27863c87afa6/go.mod h1:boTsfXsheKC2y+lKOCMpSfarhxDeIzfZG1jqGcPl3cA= +github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 h1:El6M4kTTCOh6aBiKaUGG7oYTSPP8MxqL4YI3kZKwcP4= +github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod h1:pupxD2MaaD3pAXIBCelhxNneeOaAeabZDe5s4K6zSpQ= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/gorilla/handlers v1.5.2 h1:cLTUSsNkgcwhgRqvCNmdbRWG0A3N4F+M2nWKdScwyEE= +github.com/gorilla/handlers v1.5.2/go.mod h1:dX+xVpaxdSw+q0Qek8SSsl3dfMk3jNddUkMzo0GtH0w= +github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY= +github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ= +github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674 h1:JeSE6pjso5THxAzdVpqr6/geYxZytqFMBCOtn/ujyeo= +github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674/go.mod h1:r4w70xmWCQKmi1ONH4KIaBptdivuRPyosB9RmPlGEwA= +github.com/gosuri/uitable v0.0.4 h1:IG2xLKRvErL3uhY6e1BylFzG+aJiwQviDDTfOKeKTpY= +github.com/gosuri/uitable v0.0.4/go.mod h1:tKR86bXuXPZazfOTG1FIzvjIdXzd0mo4Vtn16vt0PJo= +github.com/gregjones/httpcache v0.0.0-20190611155906-901d90724c79 h1:+ngKgrYPPJrOjhax5N+uePQ0Fh1Z7PheYoUI/0nzkPA= +github.com/gregjones/httpcache v0.0.0-20190611155906-901d90724c79/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA= +github.com/grpc-ecosystem/grpc-gateway v1.16.0 h1:gmcG1KaJ57LophUzW0Hy8NmPhnMZb4M0+kPpLofRdBo= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.3 h1:5ZPtiqj0JL5oKWmcsq4VMaAW5ukBEgSGXEN89zeH1Jo= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.3/go.mod h1:ndYquD05frm2vACXE1nsccT4oJzjhw2arTS2cpUD1PI= +github.com/h2non/parth v0.0.0-20190131123155-b4df798d6542 h1:2VTzZjLZBgl62/EtslCrtky5vbi9dd7HrQPQIx6wqiw= +github.com/h2non/parth v0.0.0-20190131123155-b4df798d6542/go.mod h1:Ow0tF8D4Kplbc8s8sSb3V2oUCygFHVp8gC3Dn6U4MNI= +github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= +github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I= +github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= +github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo= +github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM= +github.com/hashicorp/go-set/v3 v3.0.0 h1:CaJBQvQCOWoftrBcDt7Nwgo0kdpmrKxar/x2o6pV9JA= +github.com/hashicorp/go-set/v3 v3.0.0/go.mod h1:IEghM2MpE5IaNvL+D7X480dfNtxjRXZ6VMpK3C8s2ok= +github.com/hashicorp/golang-lru v0.6.0 h1:uL2shRDx7RTrOrTCUZEGP/wJUFiUI8QT6E7z5o8jga4= +github.com/hashicorp/golang-lru/arc/v2 v2.0.5 h1:l2zaLDubNhW4XO3LnliVj0GXO3+/CGNJAg1dcN2Fpfw= +github.com/hashicorp/golang-lru/arc/v2 v2.0.5/go.mod h1:ny6zBSQZi2JxIeYcv7kt2sH2PXJtirBN7RDhRpxPkxU= +github.com/hashicorp/golang-lru/v2 v2.0.5 h1:wW7h1TG88eUIJ2i69gaE3uNVtEPIagzhGvHgwfx2Vm4= +github.com/hashicorp/golang-lru/v2 v2.0.5/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM= +github.com/huandu/xstrings v1.5.0 h1:2ag3IFq9ZDANvthTwTiqSSZLjDc+BedvHPAp5tJy2TI= +github.com/huandu/xstrings v1.5.0/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE= +github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= +github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= +github.com/jmoiron/sqlx v1.4.0 h1:1PLqN7S1UYp5t4SrVVnt4nUVNemrDAtxlulVe+Qgm3o= +github.com/jmoiron/sqlx v1.4.0/go.mod h1:ZrZ7UsYB/weZdl2Bxg6jCRO9c3YHl8r3ahlKmRT4JLY= +github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= +github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= +github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= +github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= +github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= +github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= +github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo= +github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ= +github.com/klauspost/cpuid/v2 v2.0.1/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= +github.com/klauspost/cpuid/v2 v2.2.10 h1:tBs3QSyvjDyFTq3uoc/9xFpCuOsJQFNPiAhYdw2skhE= +github.com/klauspost/cpuid/v2 v2.2.10/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/kubernetes-csi/external-snapshotter/client/v8 v8.2.0 h1:Q3jQ1NkFqv5o+F8dMmHd8SfEmlcwNeo1immFApntEwE= +github.com/kubernetes-csi/external-snapshotter/client/v8 v8.2.0/go.mod h1:E3vdYxHj2C2q6qo8/Da4g7P+IcwqRZyy3gJBzYybV9Y= +github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= +github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= +github.com/lann/builder v0.0.0-20180802200727-47ae307949d0 h1:SOEGU9fKiNWd/HOJuq6+3iTQz8KNCLtVX6idSoTLdUw= +github.com/lann/builder v0.0.0-20180802200727-47ae307949d0/go.mod h1:dXGbAdH5GtBTC4WfIxhKZfyBF/HBFgRZSWwZ9g/He9o= +github.com/lann/ps v0.0.0-20150810152359-62de8c46ede0 h1:P6pPBnrTSX3DEVR4fDembhRWSsG5rVo6hYhAB/ADZrk= +github.com/lann/ps v0.0.0-20150810152359-62de8c46ede0/go.mod h1:vmVJ0l/dxyfGW6FmdpVm2joNMFikkuWg0EoCKLGUMNw= +github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw= +github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= +github.com/liggitt/tabwriter v0.0.0-20181228230101-89fcab3d43de h1:9TO3cAIGXtEhnIaL+V+BEER86oLrvS+kWobKpbJuye0= +github.com/liggitt/tabwriter v0.0.0-20181228230101-89fcab3d43de/go.mod h1:zAbeS9B/r2mtpb6U+EI2rYA5OAXxsYw6wTamcNW+zcE= +github.com/lufia/plan9stats v0.0.0-20250317134145-8bc96cf8fc35 h1:PpXWgLPs+Fqr325bN2FD2ISlRRztXibcX6e8f5FR5Dc= +github.com/lufia/plan9stats v0.0.0-20250317134145-8bc96cf8fc35/go.mod h1:autxFIvghDt3jPTLoqZ9OZ7s9qTGNAWmYCjVFWPX/zg= +github.com/mailru/easyjson v0.9.0 h1:PrnmzHw7262yW8sTBwxi1PdJA3Iw/EKBa8psRf7d9a4= +github.com/mailru/easyjson v0.9.0/go.mod h1:1+xMtQp2MRNVL/V1bOzuP3aP8VNwRW55fQUto+XFtTU= +github.com/matrix-org/complement v0.0.0-20251120181401-44111a2a8a9d h1:s2Xc9GB2E/pXdElP18h8+04Y3SmhaII7xh2YmCM7oZc= +github.com/matrix-org/complement v0.0.0-20251120181401-44111a2a8a9d/go.mod h1:HioTV089DHLBfljH9QLGifJRE4Avnyk08BXXhCwd4gs= +github.com/matrix-org/gomatrix v0.0.0-20220926102614-ceba4d9f7530 h1:kHKxCOLcHH8r4Fzarl4+Y3K5hjothkVW5z7T1dUM11U= +github.com/matrix-org/gomatrix v0.0.0-20220926102614-ceba4d9f7530/go.mod h1:/gBX06Kw0exX1HrwmoBibFA98yBk/jxKpGVeyQbff+s= +github.com/matrix-org/gomatrixserverlib v0.0.0-20250813150445-9f5070a65744 h1:5GvC2FD9O/PhuyY95iJQdNYHbDioEhMWdeMP9maDUL8= +github.com/matrix-org/gomatrixserverlib v0.0.0-20250813150445-9f5070a65744/go.mod h1:b6KVfDjXjA5Q7vhpOaMqIhFYvu5BuFVZixlNeTV/CLc= +github.com/matrix-org/util v0.0.0-20221111132719-399730281e66 h1:6z4KxomXSIGWqhHcfzExgkH3Z3UkIXry4ibJS4Aqz2Y= +github.com/matrix-org/util v0.0.0-20221111132719-399730281e66/go.mod h1:iBI1foelCqA09JJgPV0FYz4qA5dUXYOxMi57FxKBdd4= +github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE= +github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8= +github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= +github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/mattn/go-runewidth v0.0.15 h1:UNAjwbU9l54TA3KzvqLGxwWjHmMgBUVhBiTjelZgg3U= +github.com/mattn/go-runewidth v0.0.15/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= +github.com/mattn/go-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU= +github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= +github.com/matttproud/golang_protobuf_extensions v1.0.4 h1:mmDVorXM7PCGKw94cs5zkfA9PSy5pEvNWRP0ET0TIVo= +github.com/matttproud/golang_protobuf_extensions v1.0.4/go.mod h1:BSXmuO+STAnVfrANrmjBb36TMTDstsz7MSK+HVaYKv4= +github.com/miekg/dns v1.1.66 h1:FeZXOS3VCVsKnEAd+wBkjMC3D2K+ww66Cq3VnCINuJE= +github.com/miekg/dns v1.1.66/go.mod h1:jGFzBsSNbJw6z1HYut1RKBKHA9PBdxeHrZG8J+gC2WE= +github.com/minio/crc64nvme v1.0.1 h1:DHQPrYPdqK7jQG/Ls5CTBZWeex/2FMS3G5XGkycuFrY= +github.com/minio/crc64nvme v1.0.1/go.mod h1:eVfm2fAzLlxMdUGc0EEBGSMmPwmXD5XiNRpnu9J3bvg= +github.com/minio/madmin-go/v3 v3.0.100 h1:1gsy1kHgk8v9ZQyEU/L9k1ZoImFid+hlzwrOgUULB88= +github.com/minio/madmin-go/v3 v3.0.100/go.mod h1:pMLdj9OtN0CANNs5tdm6opvOlDFfj0WhbztboZAjRWE= +github.com/minio/md5-simd v1.1.2 h1:Gdi1DZK69+ZVMoNHRXJyNcxrMA4dSxoYHZSQbirFg34= +github.com/minio/md5-simd v1.1.2/go.mod h1:MzdKDxYpY2BT9XQFocsiZf/NKVtR7nkE4RoEpN+20RM= +github.com/minio/minio-go/v7 v7.0.89 h1:hx4xV5wwTUfyv8LarhJAwNecnXpoTsj9v3f3q/ZkiJU= +github.com/minio/minio-go/v7 v7.0.89/go.mod h1:2rFnGAp02p7Dddo1Fq4S2wYOfpF0MUTSeLTRC90I204= +github.com/minio/operator v0.0.0-20250423184541-6eee6a7caa70 h1:CTpACn2vNMA76TXKGufGhxVa9KIw3Qdvtx6j/l5fKeE= +github.com/minio/operator v0.0.0-20250423184541-6eee6a7caa70/go.mod h1:NL8ANdLUtI70DmG2x2EMCfoe6d3ijD1Xok/N1Tr8Czo= +github.com/mitchellh/copystructure v1.2.0 h1:vpKXTN4ewci03Vljg/q9QvCGUDttBOGBIa15WveJJGw= +github.com/mitchellh/copystructure v1.2.0/go.mod h1:qLl+cE2AmVv+CoeAwDPye/v+N2HKCj9FbZEVFJRxO9s= +github.com/mitchellh/go-wordwrap v1.0.1 h1:TLuKupo69TCn6TQSyGxwI1EblZZEsQ0vMlAFQflz0v0= +github.com/mitchellh/go-wordwrap v1.0.1/go.mod h1:R62XHJLzvMFRBbcrT7m7WgmE1eOyTSsCt+hzestvNj0= +github.com/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zxSIeXaQ= +github.com/mitchellh/reflectwalk v1.0.2/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw= +github.com/mittwald/go-helm-client v0.12.18 h1:i9cJNv/YC3ZPKUKVNYTlrOO7ZO6YFKE/ak3J5TeYHPU= +github.com/mittwald/go-helm-client v0.12.18/go.mod h1:dLl5NkdKCvwKvLIdZzg4MDbxhSKmuimdmM3WpsAzS0I= +github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0= +github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo= +github.com/moby/spdystream v0.5.0 h1:7r0J1Si3QO/kjRitvSLVVFUjxMEb/YLj6S9FF62JBCU= +github.com/moby/spdystream v0.5.0/go.mod h1:xBAYlnt/ay+11ShkdFKNAG7LsyK/tmNBVvVOwrfMgdI= +github.com/moby/sys/atomicwriter v0.1.0 h1:kw5D/EqkBwsBFi0ss9v1VG3wIkVhzGvLklJ+w3A14Sw= +github.com/moby/sys/atomicwriter v0.1.0/go.mod h1:Ul8oqv2ZMNHOceF643P6FKPXeCmYtlQMvpizfsSoaWs= +github.com/moby/sys/sequential v0.6.0 h1:qrx7XFUd/5DxtqcoH1h438hF5TmOvzC/lspjy7zgvCU= +github.com/moby/sys/sequential v0.6.0/go.mod h1:uyv8EUTrca5PnDsdMGXhZe6CCe8U/UiTWd+lL+7b/Ko= +github.com/moby/term v0.5.2 h1:6qk3FJAFDs6i/q3W/pQ97SX192qKfZgGjCQqfCJkgzQ= +github.com/moby/term v0.5.2/go.mod h1:d3djjFCrjnB+fl8NJux+EJzu0msscUP+f8it8hPkFLc= +github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= +github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/monochromegane/go-gitignore v0.0.0-20200626010858-205db1a8cc00 h1:n6/2gBQ3RWajuToeY6ZtZTIKv2v7ThUy5KKusIT0yc0= +github.com/monochromegane/go-gitignore v0.0.0-20200626010858-205db1a8cc00/go.mod h1:Pm3mSP3c5uWn86xMLZ5Sa7JB9GsEZySvHYXCTK4E9q4= +github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A= +github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc= +github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= +github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= +github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f h1:y5//uYreIhSUg3J1GEMiLbxo1LJaP8RfCpH6pymGZus= +github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f/go.mod h1:ZdcZmHo+o7JKHSa8/e818NopupXU1YMK5fe1lsApnBw= +github.com/oleiade/lane/v2 v2.0.0 h1:XW/ex/Inr+bPkLd3O240xrFOhUkTd4Wy176+Gv0E3Qw= +github.com/oleiade/lane/v2 v2.0.0/go.mod h1:i5FBPFAYSWCgLh58UkUGCChjcCzef/MI7PlQm2TKCeg= +github.com/onsi/ginkgo/v2 v2.23.4 h1:ktYTpKJAVZnDT4VjxSbiBenUjmlL/5QkBEocaWXiQus= +github.com/onsi/ginkgo/v2 v2.23.4/go.mod h1:Bt66ApGPBFzHyR+JO10Zbt0Gsp4uWxu5mIOTusL46e8= +github.com/onsi/gomega v1.37.0 h1:CdEG8g0S133B4OswTDC/5XPSzE1OeP29QOioj2PID2Y= +github.com/onsi/gomega v1.37.0/go.mod h1:8D9+Txp43QWKhM24yyOBEdpkzN8FvJyAwecBgsU4KU0= +github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= +github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= +github.com/opencontainers/image-spec v1.1.1 h1:y0fUlFfIZhPF1W537XOLg0/fcx6zcHCJwooC2xJA040= +github.com/opencontainers/image-spec v1.1.1/go.mod h1:qpqAh3Dmcf36wStyyWU+kCeDgrGnAve2nCC8+7h8Q0M= +github.com/peterbourgon/diskv v2.0.1+incompatible h1:UBdAOUP5p4RWqPBg048CAvpKN+vxiaj6gdUUzhl4XmI= +github.com/peterbourgon/diskv v2.0.1+incompatible/go.mod h1:uqqh8zWWbv1HBMNONnaR/tNboyR3/BZd58JJSHlUSCU= +github.com/phayes/freeport v0.0.0-20220201140144-74d24b5ae9f5 h1:Ii+DKncOVM8Cu1Hc+ETb5K+23HdAMvESYE3ZJ5b5cMI= +github.com/phayes/freeport v0.0.0-20220201140144-74d24b5ae9f5/go.mod h1:iIss55rKnNBTvrwdmkUpLnDpZoAHvWaiq5+iMmen4AE= +github.com/philhofer/fwd v1.1.3-0.20240916144458-20a13a1f6b7c h1:dAMKvw0MlJT1GshSTtih8C2gDs04w8dReiOGXrGLNoY= +github.com/philhofer/fwd v1.1.3-0.20240916144458-20a13a1f6b7c/go.mod h1:RqIHx9QI14HlwKwm98g9Re5prTQ6LdeRQn+gXJFxsJM= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 h1:o4JXh1EVt9k/+g42oCprj/FisM4qX9L3sZB3upGN2ZU= +github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE= +github.com/poy/onpar v1.1.2 h1:QaNrNiZx0+Nar5dLgTVp5mXkyoVFIbepjyEoGSnhbAY= +github.com/poy/onpar v1.1.2/go.mod h1:6X8FLNoxyr9kkmnlqpK6LSoiOtrO6MICtWwEuWkLjzg= +github.com/prometheus-operator/prometheus-operator/pkg/apis/monitoring v0.80.1 h1:DP+PUNVOc+Bkft8a4QunLzaZ0RspWuD3tBbcPHr2PeE= +github.com/prometheus-operator/prometheus-operator/pkg/apis/monitoring v0.80.1/go.mod h1:6x4x0t9BP35g4XcjkHE9EB3RxhyfxpdpmZKd/Qyk8+M= +github.com/prometheus/client_golang v1.22.0 h1:rb93p9lokFEsctTys46VnV1kLCDpVZ0a/Y92Vm0Zc6Q= +github.com/prometheus/client_golang v1.22.0/go.mod h1:R7ljNsLXhuQXYZYtw6GAE9AZg8Y7vEW5scdCXrWRXC0= +github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk= +github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE= +github.com/prometheus/common v0.65.0 h1:QDwzd+G1twt//Kwj/Ww6E9FQq1iVMmODnILtW1t2VzE= +github.com/prometheus/common v0.65.0/go.mod h1:0gZns+BLRQ3V6NdaerOhMbwwRbNh9hkGINtQAsP5GS8= +github.com/prometheus/procfs v0.16.0 h1:xh6oHhKwnOJKMYiYBDWmkHqQPyiY40sny36Cmx2bbsM= +github.com/prometheus/procfs v0.16.0/go.mod h1:8veyXUu3nGP7oaCxhX6yeaM5u4stL2FeMXnCqhDthZg= +github.com/prometheus/prom2json v1.4.1 h1:7McxdrHgPEOtMwWjkKtd0v5AhpR2Q6QAnlHKVxq0+tQ= +github.com/prometheus/prom2json v1.4.1/go.mod h1:CzOQykSKFxXuC7ELUZHOHQvwKesQ3eN0p2PWLhFitQM= +github.com/prometheus/prometheus v0.302.1 h1:xqVdrwrB4WNpdgJqxsz5loqFWNUZitsK8myqLuSZ6Ag= +github.com/prometheus/prometheus v0.302.1/go.mod h1:YcyCoTbUR/TM8rY3Aoeqr0AWTu/pu1Ehh+trpX3eRzg= +github.com/redis/go-redis/extra/rediscmd/v9 v9.0.5 h1:EaDatTxkdHG+U3Bk4EUr+DZ7fOGwTfezUiUJMaIcaho= +github.com/redis/go-redis/extra/rediscmd/v9 v9.0.5/go.mod h1:fyalQWdtzDBECAQFBJuQe5bzQ02jGd5Qcbgb97Flm7U= +github.com/redis/go-redis/extra/redisotel/v9 v9.0.5 h1:EfpWLLCyXw8PSM2/XNJLjI3Pb27yVE+gIAfeqp8LUCc= +github.com/redis/go-redis/extra/redisotel/v9 v9.0.5/go.mod h1:WZjPDy7VNzn77AAfnAfVjZNvfJTYfPetfZk5yoSTLaQ= +github.com/redis/go-redis/v9 v9.7.3 h1:YpPyAayJV+XErNsatSElgRZZVCwXX9QzkKYNvO7x0wM= +github.com/redis/go-redis/v9 v9.7.3/go.mod h1:bGUrSggJ9X9GUmZpZNEOQKaANxSGgOEBRltRTZHSvrA= +github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= +github.com/rivo/uniseg v0.4.4 h1:8TfxU8dW6PdqD27gjM8MVNuicgxIjxpm4K7x4jp8sis= +github.com/rivo/uniseg v0.4.4/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= +github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII= +github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o= +github.com/rs/xid v1.6.0 h1:fV591PaemRlL6JfRxGDEPl69wICngIQ3shQtzfy2gxU= +github.com/rs/xid v1.6.0/go.mod h1:7XoLgs4eV+QndskICGsho+ADou8ySMSjJKDIan90Nz0= +github.com/rubenv/sql-migrate v1.8.0 h1:dXnYiJk9k3wetp7GfQbKJcPHjVJL6YK19tKj8t2Ns0o= +github.com/rubenv/sql-migrate v1.8.0/go.mod h1:F2bGFBwCU+pnmbtNYDeKvSuvL6lBVtXDXUUv5t+u1qw= +github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= +github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/safchain/ethtool v0.5.10 h1:Im294gZtuf4pSGJRAOGKaASNi3wMeFaGaWuSaomedpc= +github.com/safchain/ethtool v0.5.10/go.mod h1:w9jh2Lx7YBR4UwzLkzCmWl85UY0W2uZdd7/DckVE5+c= +github.com/secure-io/sio-go v0.3.1 h1:dNvY9awjabXTYGsTF1PiCySl9Ltofk9GA3VdWlo7rRc= +github.com/secure-io/sio-go v0.3.1/go.mod h1:+xbkjDzPjwh4Axd07pRKSNriS9SCiYksWnZqdnfpQxs= +github.com/sergi/go-diff v1.2.0 h1:XU+rvMAioB0UC3q1MFrIQy4Vo5/4VsRDQQXHsEya6xQ= +github.com/sergi/go-diff v1.2.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM= +github.com/shirou/gopsutil/v3 v3.24.5 h1:i0t8kL+kQTvpAYToeuiVk3TgDeKOFioZO3Ztz/iZ9pI= +github.com/shirou/gopsutil/v3 v3.24.5/go.mod h1:bsoOS1aStSs9ErQ1WWfxllSeS1K5D+U30r2NfcubMVk= +github.com/shoenig/go-m1cpu v0.1.6 h1:nxdKQNcEB6vzgA2E2bvzKIYRuNj7XNJ4S/aRSwKzFtM= +github.com/shoenig/go-m1cpu v0.1.6/go.mod h1:1JJMcUBvfNwpq05QDQVAnx3gUHr9IYF7GNg9SUEw2VQ= +github.com/shoenig/test v1.11.0 h1:NoPa5GIoBwuqzIviCrnUJa+t5Xb4xi5Z+zODJnIDsEQ= +github.com/shoenig/test v1.11.0/go.mod h1:UxJ6u/x2v/TNs/LoLxBNJRV9DiwBBKYxXSyczsBHFoI= +github.com/shopspring/decimal v1.4.0 h1:bxl37RwXBklmTi0C79JfXCEBD1cqqHt0bbgBAGFp81k= +github.com/shopspring/decimal v1.4.0/go.mod h1:gawqmDU56v4yIKSwfBSFip1HdCCXN8/+DMd9qYNcwME= +github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= +github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= +github.com/spf13/cast v1.7.0 h1:ntdiHjuueXFgm5nzDRdOS4yfT43P5Fnud6DH50rz/7w= +github.com/spf13/cast v1.7.0/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo= +github.com/spf13/cobra v1.9.1 h1:CXSaggrXdbHK9CF+8ywj8Amf7PBRmPCOJugH954Nnlo= +github.com/spf13/cobra v1.9.1/go.mod h1:nDyEzZ8ogv936Cinf6g1RU9MRY64Ir93oCnqb9wxYW0= +github.com/spf13/pflag v1.0.6 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o= +github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= +github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= +github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= +github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/thoas/go-funk v0.9.3 h1:7+nAEx3kn5ZJcnDm2Bh23N2yOtweO14bi//dvRtgLpw= +github.com/thoas/go-funk v0.9.3/go.mod h1:+IWnUfUmFO1+WVYQWQtIJHeRRdaIyyYglZN7xzUPe4Q= +github.com/tidwall/gjson v1.14.2/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= +github.com/tidwall/gjson v1.18.0 h1:FIDeeyB800efLX89e5a8Y0BNH+LOngJyGrIWxG2FKQY= +github.com/tidwall/gjson v1.18.0/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= +github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA= +github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM= +github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= +github.com/tidwall/pretty v1.2.1 h1:qjsOFOWWQl+N3RsoF5/ssm1pHmJJwhjlSbZ51I6wMl4= +github.com/tidwall/pretty v1.2.1/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= +github.com/tidwall/sjson v1.2.5 h1:kLy8mja+1c9jlljvWTlSazM7cKDRfJuR/bOJhcY5NcY= +github.com/tidwall/sjson v1.2.5/go.mod h1:Fvgq9kS/6ociJEDnK0Fk1cpYF4FIW6ZF7LAe+6jwd28= +github.com/tinylib/msgp v1.2.5 h1:WeQg1whrXRFiZusidTQqzETkRpGjFjcIhW6uqWH09po= +github.com/tinylib/msgp v1.2.5/go.mod h1:ykjzy2wzgrlvpDCRc4LA8UXy6D8bzMSuAF3WD57Gok0= +github.com/tklauser/go-sysconf v0.3.15 h1:VE89k0criAymJ/Os65CSn1IXaol+1wrsFHEB8Ol49K4= +github.com/tklauser/go-sysconf v0.3.15/go.mod h1:Dmjwr6tYFIseJw7a3dRLJfsHAMXZ3nEnL/aZY+0IuI4= +github.com/tklauser/numcpus v0.10.0 h1:18njr6LDBk1zuna922MgdjQuJFjrdppsZG60sHGfjso= +github.com/tklauser/numcpus v0.10.0/go.mod h1:BiTKazU708GQTYF4mB+cmlpT2Is1gLk7XVuEeem8LsQ= +github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= +github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= +github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU= +github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb h1:zGWFAtiMcyryUHoUjUJX0/lt1H2+i2Ka2n+D3DImSNo= +github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU= +github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 h1:EzJWgHovont7NscjpAxXsDA8S8BMYve8Y5+7cuRE7R0= +github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1:GwrjFmJcFw6At/Gs6z4yjiIwzuJ1/+UwLxMQDVQXShQ= +github.com/xeipuuv/gojsonschema v1.2.0 h1:LhYJRs+L4fBtjZUfuSZIKGeVu0QRy8e5Xi7D17UxZ74= +github.com/xeipuuv/gojsonschema v1.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQluxsYJ78Id3Y= +github.com/xlab/treeprint v1.2.0 h1:HzHnuAF1plUN2zGlAFHbSQP2qJ0ZAD3XF5XD7OesXRQ= +github.com/xlab/treeprint v1.2.0/go.mod h1:gj5Gd3gPdKtR1ikdDK6fnFLdmIS0X30kTTuNd/WEJu0= +github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0= +github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0= +go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= +go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= +go.opentelemetry.io/contrib/bridges/prometheus v0.57.0 h1:UW0+QyeyBVhn+COBec3nGhfnFe5lwB0ic1JBVjzhk0w= +go.opentelemetry.io/contrib/bridges/prometheus v0.57.0/go.mod h1:ppciCHRLsyCio54qbzQv0E4Jyth/fLWDTJYfvWpcSVk= +go.opentelemetry.io/contrib/exporters/autoexport v0.57.0 h1:jmTVJ86dP60C01K3slFQa2NQ/Aoi7zA+wy7vMOKD9H4= +go.opentelemetry.io/contrib/exporters/autoexport v0.57.0/go.mod h1:EJBheUMttD/lABFyLXhce47Wr6DPWYReCzaZiXadH7g= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0 h1:F7Jx+6hwnZ41NSFTO5q4LYDtJRXBf2PD0rNBkeB/lus= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0/go.mod h1:UHB22Z8QsdRDrnAtX4PntOl36ajSxcdUMt1sF7Y6E7Q= +go.opentelemetry.io/otel v1.36.0 h1:UumtzIklRBY6cI/lllNZlALOF5nNIzJVb16APdvgTXg= +go.opentelemetry.io/otel v1.36.0/go.mod h1:/TcFMXYjyRNh8khOAO9ybYkqaDBb/70aVwkNML4pP8E= +go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.8.0 h1:WzNab7hOOLzdDF/EoWCt4glhrbMPVMOO5JYTmpz36Ls= +go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.8.0/go.mod h1:hKvJwTzJdp90Vh7p6q/9PAOd55dI6WA6sWj62a/JvSs= +go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.8.0 h1:S+LdBGiQXtJdowoJoQPEtI52syEP/JYBUpjO49EQhV8= +go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.8.0/go.mod h1:5KXybFvPGds3QinJWQT7pmXf+TN5YIa7CNYObWRkj50= +go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.32.0 h1:j7ZSD+5yn+lo3sGV69nW04rRR0jhYnBwjuX3r0HvnK0= +go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.32.0/go.mod h1:WXbYJTUaZXAbYd8lbgGuvih0yuCfOFC5RJoYnoLcGz8= +go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.32.0 h1:t/Qur3vKSkUCcDVaSumWF2PKHt85pc7fRvFuoVT8qFU= +go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.32.0/go.mod h1:Rl61tySSdcOJWoEgYZVtmnKdA0GeKrSqkHC1t+91CH8= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.36.0 h1:dNzwXjZKpMpE2JhmO+9HsPl42NIXFIFSUSSs0fiqra0= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.36.0/go.mod h1:90PoxvaEB5n6AOdZvi+yWJQoE95U8Dhhw2bSyRqnTD0= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.34.0 h1:tgJ0uaNS4c98WRNUEx5U3aDlrDOI5Rs+1Vifcw4DJ8U= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.34.0/go.mod h1:U7HYyW0zt/a9x5J1Kjs+r1f/d4ZHnYFclhYY2+YbeoE= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.34.0 h1:BEj3SPM81McUZHYjRS5pEgNgnmzGJ5tRpU5krWnV8Bs= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.34.0/go.mod h1:9cKLGBDzI/F3NoHLQGm4ZrYdIHsvGt6ej6hUowxY0J4= +go.opentelemetry.io/otel/exporters/prometheus v0.54.0 h1:rFwzp68QMgtzu9PgP3jm9XaMICI6TsofWWPcBDKwlsU= +go.opentelemetry.io/otel/exporters/prometheus v0.54.0/go.mod h1:QyjcV9qDP6VeK5qPyKETvNjmaaEc7+gqjh4SS0ZYzDU= +go.opentelemetry.io/otel/exporters/stdout/stdoutlog v0.8.0 h1:CHXNXwfKWfzS65yrlB2PVds1IBZcdsX8Vepy9of0iRU= +go.opentelemetry.io/otel/exporters/stdout/stdoutlog v0.8.0/go.mod h1:zKU4zUgKiaRxrdovSS2amdM5gOc59slmo/zJwGX+YBg= +go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.32.0 h1:SZmDnHcgp3zwlPBS2JX2urGYe/jBKEIT6ZedHRUyCz8= +go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.32.0/go.mod h1:fdWW0HtZJ7+jNpTKUR0GpMEDP69nR8YBJQxNiVCE3jk= +go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.32.0 h1:cC2yDI3IQd0Udsux7Qmq8ToKAx1XCilTQECZ0KDZyTw= +go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.32.0/go.mod h1:2PD5Ex6z8CFzDbTdOlwyNIUywRr1DN0ospafJM1wJ+s= +go.opentelemetry.io/otel/log v0.8.0 h1:egZ8vV5atrUWUbnSsHn6vB8R21G2wrKqNiDt3iWertk= +go.opentelemetry.io/otel/log v0.8.0/go.mod h1:M9qvDdUTRCopJcGRKg57+JSQ9LgLBrwwfC32epk5NX8= +go.opentelemetry.io/otel/metric v1.36.0 h1:MoWPKVhQvJ+eeXWHFBOPoBOi20jh6Iq2CcCREuTYufE= +go.opentelemetry.io/otel/metric v1.36.0/go.mod h1:zC7Ks+yeyJt4xig9DEw9kuUFe5C3zLbVjV2PzT6qzbs= +go.opentelemetry.io/otel/sdk v1.36.0 h1:b6SYIuLRs88ztox4EyrvRti80uXIFy+Sqzoh9kFULbs= +go.opentelemetry.io/otel/sdk v1.36.0/go.mod h1:+lC+mTgD+MUWfjJubi2vvXWcVxyr9rmlshZni72pXeY= +go.opentelemetry.io/otel/sdk/log v0.8.0 h1:zg7GUYXqxk1jnGF/dTdLPrK06xJdrXgqgFLnI4Crxvs= +go.opentelemetry.io/otel/sdk/log v0.8.0/go.mod h1:50iXr0UVwQrYS45KbruFrEt4LvAdCaWWgIrsN3ZQggo= +go.opentelemetry.io/otel/sdk/metric v1.36.0 h1:r0ntwwGosWGaa0CrSt8cuNuTcccMXERFwHX4dThiPis= +go.opentelemetry.io/otel/sdk/metric v1.36.0/go.mod h1:qTNOhFDfKRwX0yXOqJYegL5WRaW376QbB7P4Pb0qva4= +go.opentelemetry.io/otel/trace v1.36.0 h1:ahxWNuqZjpdiFAyrIoQ4GIiAIhxAunQR6MUoKrsNd4w= +go.opentelemetry.io/otel/trace v1.36.0/go.mod h1:gQ+OnDZzrybY4k4seLzPAWNwVBBVlF2szhehOBB/tGA= +go.opentelemetry.io/proto/otlp v1.7.0 h1:jX1VolD6nHuFzOYso2E73H85i92Mv8JQYk0K9vz09os= +go.opentelemetry.io/proto/otlp v1.7.0/go.mod h1:fSKjH6YJ7HDlwzltzyMj036AJ3ejJLCgCSHGj4efDDo= +go.uber.org/automaxprocs v1.6.0 h1:O3y2/QNTOdbF+e/dpXNNW7Rx2hZ4sTIPyybbxyNqTUs= +go.uber.org/automaxprocs v1.6.0/go.mod h1:ifeIMSnPZuznNm6jmdzmU3/bfk01Fe2fotchwEFJ8r8= +go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= +go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= +go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= +go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= +go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8= +go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20200302210943-78000ba7a073/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.45.0 h1:jMBrvKuj23MTlT0bQEOBcAE0mjg8mK9RXFhRH6nyF3Q= +golang.org/x/crypto v0.45.0/go.mod h1:XTGrrkGJve7CYK7J8PEww4aY7gM3qMCElcJQ8n8JdX4= +golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 h1:2dVuKD2vS7b0QIHQbpyTISPd0LeHDbnYEryqj5Q1ug8= +golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56/go.mod h1:M4RDyNAINzryxdtnbRXRL/OHtkFuWGRjvuhBJpk2IlY= +golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.29.0 h1:HV8lRxZC4l2cr3Zq1LvtOsi/ThTgWnUk/y64QSs8GwA= +golang.org/x/mod v0.29.0/go.mod h1:NyhrlYXJ2H4eJiRy/WDBO6HMqZQ6q9nk4JzS3NuCK+w= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.47.0 h1:Mx+4dIFzqraBXUugkia1OOvlD6LemFo1ALMHjrXDOhY= +golang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU= +golang.org/x/oauth2 v0.30.0 h1:dnDm7JmhM45NNpd8FDDeLhK6FwqbOf4MLCM9zb1BOHI= +golang.org/x/oauth2 v0.30.0/go.mod h1:B++QgG3ZKulg6sRPGD/mqlHQs5rB3Ml9erfeDY7xKlU= +golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.18.0 h1:kr88TuHDroi+UVf+0hZnirlk8o8T+4MrK6mr60WkH/I= +golang.org/x/sync v0.18.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201204225414-ed752295db88/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc= +golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/term v0.37.0 h1:8EGAD0qCmHYZg6J17DvsMy9/wJ7/D/4pV/wfnld5lTU= +golang.org/x/term v0.37.0/go.mod h1:5pB4lxRNYYVZuTLmy8oR2BH8dflOR+IbTYFD8fi3254= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.31.0 h1:aC8ghyu4JhP8VojJ2lEHBnochRno1sgL6nEi9WGFGMM= +golang.org/x/text v0.31.0/go.mod h1:tKRAlv61yKIjGGHX/4tP1LTbc13YSec1pxVEWXzfoeM= +golang.org/x/time v0.11.0 h1:/bpjEDfN9tkoN/ryeYHnv5hcMlc8ncjMcM4XBk5NWV0= +golang.org/x/time v0.11.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0= +golang.org/x/tools v0.38.0 h1:Hx2Xv8hISq8Lm16jvBZ2VQf+RLmbd7wVUsALibYI/IQ= +golang.org/x/tools v0.38.0/go.mod h1:yEsQ/d/YK8cjh0L6rZlY8tgtlKiBNTL14pGDJPJpYQs= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +gomodules.xyz/jsonpatch/v2 v2.4.0 h1:Ci3iUJyx9UeRx7CeFN8ARgGbkESwJK+KB9lLcWxY/Zw= +gomodules.xyz/jsonpatch/v2 v2.4.0/go.mod h1:AH3dM2RI6uoBZxn3LVrfvJ3E0/9dG4cSrbuBJT4moAY= +google.golang.org/genproto v0.0.0-20240123012728-ef4313101c80 h1:KAeGQVN3M9nD0/bQXnr/ClcEMJ968gUXJQ9pwfSynuQ= +google.golang.org/genproto/googleapis/api v0.0.0-20250528174236-200df99c418a h1:SGktgSolFCo75dnHJF2yMvnns6jCmHFJ0vE4Vn2JKvQ= +google.golang.org/genproto/googleapis/api v0.0.0-20250528174236-200df99c418a/go.mod h1:a77HrdMjoeKbnd2jmgcWdaS++ZLZAEq3orIOAEIKiVw= +google.golang.org/genproto/googleapis/rpc v0.0.0-20250528174236-200df99c418a h1:v2PbRU4K3llS09c7zodFpNePeamkAwG3mPrAery9VeE= +google.golang.org/genproto/googleapis/rpc v0.0.0-20250528174236-200df99c418a/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A= +google.golang.org/grpc v1.72.2 h1:TdbGzwb82ty4OusHWepvFWGLgIbNo1/SUynEN0ssqv8= +google.golang.org/grpc v1.72.2/go.mod h1:wH5Aktxcg25y1I3w7H69nHfXdOG3UiadoBtjh3izSDM= +google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY= +google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/evanphx/json-patch.v4 v4.12.0 h1:n6jtcsulIzXPJaxegRbvFNNrZDjbij7ny3gmSPG+6V4= +gopkg.in/evanphx/json-patch.v4 v4.12.0/go.mod h1:p8EYWUEYMpynmqDbY58zCKCFZw8pRWMG4EsWvDvM72M= +gopkg.in/h2non/gock.v1 v1.1.2 h1:jBbHXgGBK/AoPVfJh5x4r/WxIrElvbLel8TCZkkZJoY= +gopkg.in/h2non/gock.v1 v1.1.2/go.mod h1:n7UGz/ckNChHiK05rDoiC4MYSunEC/lyaUm2WWaDva0= +gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc= +gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= +gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gotest.tools/v3 v3.4.0 h1:ZazjZUfuVeZGLAmlKKuyv3IKP5orXcwtOwDQH6YVr6o= +gotest.tools/v3 v3.4.0/go.mod h1:CtbdzLSsqVhDgMtKsx03ird5YTGB3ar27v0u/yKBW5g= +helm.sh/helm/v3 v3.18.4 h1:pNhnHM3nAmDrxz6/UC+hfjDY4yeDATQCka2/87hkZXQ= +helm.sh/helm/v3 v3.18.4/go.mod h1:WVnwKARAw01iEdjpEkP7Ii1tT1pTPYfM1HsakFKM3LI= +k8s.io/api v0.33.2 h1:YgwIS5jKfA+BZg//OQhkJNIfie/kmRsO0BmNaVSimvY= +k8s.io/api v0.33.2/go.mod h1:fhrbphQJSM2cXzCWgqU29xLDuks4mu7ti9vveEnpSXs= +k8s.io/apiextensions-apiserver v0.33.2 h1:6gnkIbngnaUflR3XwE1mCefN3YS8yTD631JXQhsU6M8= +k8s.io/apiextensions-apiserver v0.33.2/go.mod h1:IvVanieYsEHJImTKXGP6XCOjTwv2LUMos0YWc9O+QP8= +k8s.io/apimachinery v0.33.2 h1:IHFVhqg59mb8PJWTLi8m1mAoepkUNYmptHsV+Z1m5jY= +k8s.io/apimachinery v0.33.2/go.mod h1:BHW0YOu7n22fFv/JkYOEfkUYNRN0fj0BlvMFWA7b+SM= +k8s.io/apiserver v0.33.2 h1:KGTRbxn2wJagJowo29kKBp4TchpO1DRO3g+dB/KOJN4= +k8s.io/apiserver v0.33.2/go.mod h1:9qday04wEAMLPWWo9AwqCZSiIn3OYSZacDyu/AcoM/M= +k8s.io/cli-runtime v0.33.2 h1:koNYQKSDdq5AExa/RDudXMhhtFasEg48KLS2KSAU74Y= +k8s.io/cli-runtime v0.33.2/go.mod h1:gnhsAWpovqf1Zj5YRRBBU7PFsRc6NkEkwYNQE+mXL88= +k8s.io/client-go v0.33.2 h1:z8CIcc0P581x/J1ZYf4CNzRKxRvQAwoAolYPbtQes+E= +k8s.io/client-go v0.33.2/go.mod h1:9mCgT4wROvL948w6f6ArJNb7yQd7QsvqavDeZHvNmHo= +k8s.io/component-base v0.33.2 h1:sCCsn9s/dG3ZrQTX/Us0/Sx2R0G5kwa0wbZFYoVp/+0= +k8s.io/component-base v0.33.2/go.mod h1:/41uw9wKzuelhN+u+/C59ixxf4tYQKW7p32ddkYNe2k= +k8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk= +k8s.io/klog/v2 v2.130.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE= +k8s.io/kube-openapi v0.0.0-20250318190949-c8a335a9a2ff h1:/usPimJzUKKu+m+TE36gUyGcf03XZEP0ZIKgKj35LS4= +k8s.io/kube-openapi v0.0.0-20250318190949-c8a335a9a2ff/go.mod h1:5jIi+8yX4RIb8wk3XwBo5Pq2ccx4FP10ohkbSKCZoK8= +k8s.io/kubectl v0.33.2 h1:7XKZ6DYCklu5MZQzJe+CkCjoGZwD1wWl7t/FxzhMz7Y= +k8s.io/kubectl v0.33.2/go.mod h1:8rC67FB8tVTYraovAGNi/idWIK90z2CHFNMmGJZJ3KI= +k8s.io/utils v0.0.0-20250502105355-0f33e8f1c979 h1:jgJW5IePPXLGB8e/1wvd0Ich9QE97RvvF3a8J3fP/Lg= +k8s.io/utils v0.0.0-20250502105355-0f33e8f1c979/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= +oras.land/oras-go/v2 v2.6.0 h1:X4ELRsiGkrbeox69+9tzTu492FMUu7zJQW6eJU+I2oc= +oras.land/oras-go/v2 v2.6.0/go.mod h1:magiQDfG6H1O9APp+rOsvCPcW1GD2MM7vgnKY0Y+u1o= +sigs.k8s.io/controller-runtime v0.21.0 h1:CYfjpEuicjUecRk+KAeyYh+ouUBn4llGyDYytIGcJS8= +sigs.k8s.io/controller-runtime v0.21.0/go.mod h1:OSg14+F65eWqIu4DceX7k/+QRAbTTvxeQSNSOQpukWM= +sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8 h1:gBQPwqORJ8d8/YNZWEjoZs7npUVDpVXUUOFfW6CgAqE= +sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8/go.mod h1:mdzfpAEoE6DHQEN0uh9ZbOCuHbLK5wOm7dK4ctXE9Tg= +sigs.k8s.io/kustomize/api v0.19.0 h1:F+2HB2mU1MSiR9Hp1NEgoU2q9ItNOaBJl0I4Dlus5SQ= +sigs.k8s.io/kustomize/api v0.19.0/go.mod h1:/BbwnivGVcBh1r+8m3tH1VNxJmHSk1PzP5fkP6lbL1o= +sigs.k8s.io/kustomize/kyaml v0.19.0 h1:RFge5qsO1uHhwJsu3ipV7RNolC7Uozc0jUBC/61XSlA= +sigs.k8s.io/kustomize/kyaml v0.19.0/go.mod h1:FeKD5jEOH+FbZPpqUghBP8mrLjJ3+zD3/rf9NNu1cwY= +sigs.k8s.io/randfill v0.0.0-20250304075658-069ef1bbf016/go.mod h1:XeLlZ/jmk4i1HRopwe7/aU3H5n1zNUcX6TM94b3QxOY= +sigs.k8s.io/randfill v1.0.0 h1:JfjMILfT8A6RbawdsK2JXGBR5AQVfd+9TbzrlneTyrU= +sigs.k8s.io/randfill v1.0.0/go.mod h1:XeLlZ/jmk4i1HRopwe7/aU3H5n1zNUcX6TM94b3QxOY= +sigs.k8s.io/structured-merge-diff/v4 v4.6.0 h1:IUA9nvMmnKWcj5jl84xn+T5MnlZKThmUW1TdblaLVAc= +sigs.k8s.io/structured-merge-diff/v4 v4.6.0/go.mod h1:dDy58f92j70zLsuZVuUX5Wp9vtxXpaZnkPGWeqDfCps= +sigs.k8s.io/yaml v1.4.0 h1:Mk1wCc2gy/F0THH0TAp1QYyJNzRm2KCLy3o5ASXVI5E= +sigs.k8s.io/yaml v1.4.0/go.mod h1:Ejl7/uTz7PSA4eKMyQCUTnhZYNmLIl+5c2lQPGR2BPY= diff --git a/complement/tests/federation_test.go b/complement/tests/federation_test.go new file mode 100644 index 00000000000..d423792a72f --- /dev/null +++ b/complement/tests/federation_test.go @@ -0,0 +1,65 @@ +// This file is licensed under the Affero General Public License (AGPL) version 3. +// +// Copyright (C) 2026 Element Creations Ltd +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as +// published by the Free Software Foundation, either version 3 of the +// License, or (at your option) any later version. +// +// See the GNU Affero General Public License for more details: +// . + +package synapse_tests + +import ( + "testing" + + "github.com/matrix-org/complement" + "github.com/matrix-org/complement/client" + "github.com/matrix-org/complement/helpers" + "github.com/matrix-org/gomatrixserverlib/spec" +) + +// Stub test to ensure that homeservers can communicate with each other (federation works correctly). +// +// TODO: This test will disappear once we have other real Synapse specific tests in +// place. This is simply here as an example without bloating the PR with some specific +// new tests. +func TestFederation(t *testing.T) { + // Create two homeservers + deployment := complement.Deploy(t, 2) + defer deployment.Destroy(t) + + alice := deployment.Register(t, "hs1", helpers.RegistrationOpts{}) + bob := deployment.Register(t, "hs2", helpers.RegistrationOpts{}) + + aliceRoomID := alice.MustCreateRoom(t, map[string]any{ + "preset": "public_chat", + }) + bobRoomID := bob.MustCreateRoom(t, map[string]any{ + "preset": "public_chat", + }) + + t.Run("parallel", func(t *testing.T) { + t.Run("HS1 -> HS2", func(t *testing.T) { + t.Parallel() + + alice.MustJoinRoom(t, bobRoomID, []spec.ServerName{ + deployment.GetFullyQualifiedHomeserverName(t, "hs2"), + }) + + bob.MustSyncUntil(t, client.SyncReq{}, client.SyncJoinedTo(alice.UserID, bobRoomID)) + }) + + t.Run("HS2 -> HS1", func(t *testing.T) { + t.Parallel() + + bob.MustJoinRoom(t, aliceRoomID, []spec.ServerName{ + deployment.GetFullyQualifiedHomeserverName(t, "hs1"), + }) + + alice.MustSyncUntil(t, client.SyncReq{}, client.SyncJoinedTo(bob.UserID, aliceRoomID)) + }) + }) +} diff --git a/complement/tests/main_test.go b/complement/tests/main_test.go new file mode 100644 index 00000000000..1d62900d384 --- /dev/null +++ b/complement/tests/main_test.go @@ -0,0 +1,23 @@ +// This file is licensed under the Affero General Public License (AGPL) version 3. +// +// Copyright (C) 2026 Element Creations Ltd +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as +// published by the Free Software Foundation, either version 3 of the +// License, or (at your option) any later version. +// +// See the GNU Affero General Public License for more details: +// . + +package synapse_tests + +import ( + "testing" + + "github.com/matrix-org/complement" +) + +func TestMain(m *testing.M) { + complement.TestMain(m, "synapse") +} diff --git a/scripts-dev/complement.sh b/scripts-dev/complement.sh index 2447e0dc7bb..dc2262a2a35 100755 --- a/scripts-dev/complement.sh +++ b/scripts-dev/complement.sh @@ -47,6 +47,9 @@ usage() { cat >&2 <... Run the complement test suite on Synapse. + --in-repo + Whether to run the in-repo suite of Complement tests (see `./complement` in this project) + vs the Complement tests from the Complement repo. -f, --fast Skip rebuilding the docker images, and just use the most recent @@ -82,6 +85,7 @@ main() { # parse our arguments skip_docker_build="" skip_complement_run="" + use_in_repo_tests="" while [ $# -ge 1 ]; do arg=$1 case "$arg" in @@ -89,6 +93,9 @@ main() { usage return 1 ;; + "--in-repo") + use_in_repo_tests=1 + ;; "-f"|"--fast") skip_docker_build=1 ;; @@ -216,7 +223,10 @@ main() { echo "Skipping Docker image build as requested." fi - test_packages=( + # Default set of Complement tests to run from the Complement repo + # + # We pick and choose the specific MSC's that Synapse supports. + default_complement_test_packages=( ./tests/csapi ./tests ./tests/msc3874 @@ -233,7 +243,15 @@ main() { # Export the list of test packages as a space-separated environment variable, so other # scripts can use it. - export SYNAPSE_SUPPORTED_COMPLEMENT_TEST_PACKAGES="${test_packages[@]}" + export SYNAPSE_SUPPORTED_COMPLEMENT_TEST_PACKAGES="${default_complement_test_packages[@]}" + + # Default set of Complement tests to run when using the in-repo test suite. Most + # likely, this should be all tests. + # + # Relative to the `./complement` repo in this project + default_in_repo_complement_test_packages=( + ./tests/... + ) export COMPLEMENT_BASE_IMAGE=complement-synapse if [ -n "$use_editable_synapse" ]; then @@ -316,11 +334,26 @@ main() { echo "Skipping Complement run as requested." return 0 fi + + # Print out the executed commands so it's more obvious what's happening at the end here. + # Things are slightly ambiguous with the in-repo vs Complement tests. + set -x - # Run the tests! - echo "Running Complement with ${test_args[@]} $@ ${test_packages[@]}" - cd "$COMPLEMENT_DIR" - go test "${test_args[@]}" "$@" "${test_packages[@]}" + if [ -n "$use_in_repo_tests" ]; then + # Run the suite of Complement tests in the `./complement` directory in this repo + cd "./complement" + go test "${test_args[@]}" "$@" "${default_in_repo_complement_test_packages[@]}" + else + # Run the tests (from the Complement repo)! + cd "$COMPLEMENT_DIR" + go test "${test_args[@]}" "$@" "${default_complement_test_packages[@]}" + fi + + # We don't need to print out executed commands anymore + # + # This is just `set +x` without printing `+ set +x` to the console (via + # https://stackoverflow.com/questions/13195655/bash-set-x-without-it-being-printed/19226038#19226038) + { set +x; } 2>/dev/null } main "$@" diff --git a/scripts-dev/lint.sh b/scripts-dev/lint.sh index d5e10d42926..b10cf8b80a3 100755 --- a/scripts-dev/lint.sh +++ b/scripts-dev/lint.sh @@ -139,3 +139,14 @@ mypy # Generate configuration documentation from the JSON Schema ./scripts-dev/gen_config_documentation.py schema/synapse-config.schema.yaml > docs/usage/configuration/config_documentation.md + +# Lint/format the in-repo Complement test code (Go) +pushd ./complement +# Run golangci-lint with: +# - The `run` command will lint the code +# - `--fix`: Will apply any suggested fixes like autofixes from `linters` *and* +# `formatters` which always produce suggested fixes that are equivalent to running +# `golangci-lint fmt`. +# - `--max-issues-per-linter=0`: Show all issues, don't limit the number reported +go run github.com/golangci/golangci-lint/v2/cmd/golangci-lint@v2.6.1 run ./... --fix --max-issues-per-linter=0 +popd From d423ab0cd5c160b6f95e541e7be42309f49631d7 Mon Sep 17 00:00:00 2001 From: Quentin Gliech Date: Tue, 10 Feb 2026 13:40:56 +0100 Subject: [PATCH 010/121] 1.147.0 --- CHANGES.md | 7 +++++++ debian/changelog | 6 ++++++ pyproject.toml | 2 +- 3 files changed, 14 insertions(+), 1 deletion(-) diff --git a/CHANGES.md b/CHANGES.md index 9c688402ef0..9b683bfc699 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,3 +1,10 @@ +# synapse 1.147.0 (2026-02-10) + +No significant changes since 1.147.0rc1. + + + + # Synapse 1.147.0rc1 (2026-02-03) ## Bugfixes diff --git a/debian/changelog b/debian/changelog index 8168bde3a21..36a2921552f 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,3 +1,9 @@ +matrix-synapse-py3 (1.147.0) stable; urgency=medium + + * New synapse release 1.147.0. + + -- Synapse Packaging team Tue, 10 Feb 2026 12:39:58 +0000 + matrix-synapse-py3 (1.147.0~rc1) stable; urgency=medium * New Synapse release 1.147.0rc1. diff --git a/pyproject.toml b/pyproject.toml index a26247ab1eb..df10bdf19bd 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "matrix-synapse" -version = "1.147.0rc1" +version = "1.147.0" description = "Homeserver for the Matrix decentralised comms protocol" readme = "README.rst" authors = [ From a6152cec043edcc1be7647ee95ca5324f8c43780 Mon Sep 17 00:00:00 2001 From: Quentin Gliech Date: Tue, 10 Feb 2026 13:41:53 +0100 Subject: [PATCH 011/121] Fixup changelog --- CHANGES.md | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index 9b683bfc699..2aac866df81 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,10 +1,7 @@ -# synapse 1.147.0 (2026-02-10) +# Synapse 1.147.0 (2026-02-10) No significant changes since 1.147.0rc1. - - - # Synapse 1.147.0rc1 (2026-02-03) ## Bugfixes From 52fb6e98acf8c41662d070c0803a310f7f27bc7b Mon Sep 17 00:00:00 2001 From: Olivier 'reivilibre Date: Wed, 11 Feb 2026 12:41:38 +0000 Subject: [PATCH 012/121] Support sending and receiving MSC4354 Sticky Event metadata. (#19365) Part of: MSC4354 whose experimental feature tracking issue is https://github.com/element-hq/synapse/issues/19409 Follows: #19340 (a necessary bugfix for `/event/` to set this metadata) Partially supersedes: #18968 This PR implements the first batch of work to support MSC4354 Sticky Events. Sticky events are events that have been configured with a finite 'stickiness' duration, capped to 1 hour per current MSC draft. Whilst an event is sticky, we provide stronger delivery guarantees for the event, both to our clients and to remote homeservers, essentially making it reliable delivery as long as we have a functional connection to the client/server and until the stickiness expires. This PR merely supports creating sticky events and receiving the sticky TTL metadata in clients. It is not suitable for trialling sticky events since none of the other semantics are implemented. Contains a temporary SQLite workaround due to a bug in our supported version enforcement: https://github.com/element-hq/synapse/issues/19452 --------- Signed-off-by: Olivier 'reivilibre Co-authored-by: Eric Eastwood --- changelog.d/19365.feature | 1 + .../conf/workers-shared-extra.yaml.j2 | 2 + synapse/api/constants.py | 43 ++- synapse/app/generic_worker.py | 2 + synapse/config/experimental.py | 6 + synapse/config/workers.py | 4 +- synapse/events/__init__.py | 30 +- synapse/events/builder.py | 10 +- synapse/handlers/delayed_events.py | 13 +- synapse/notifier.py | 1 + synapse/replication/tcp/client.py | 11 +- synapse/replication/tcp/handler.py | 7 + synapse/replication/tcp/streams/__init__.py | 3 + synapse/replication/tcp/streams/_base.py | 45 +++ synapse/rest/client/room.py | 29 +- synapse/rest/client/versions.py | 2 + synapse/storage/databases/main/__init__.py | 2 + .../storage/databases/main/delayed_events.py | 9 +- synapse/storage/databases/main/events.py | 11 + .../storage/databases/main/sticky_events.py | 322 ++++++++++++++++++ .../schema/main/delta/93/01_sticky_events.sql | 60 ++++ .../93/01_sticky_events_seq.sql.postgres | 18 + synapse/streams/events.py | 3 + synapse/types/__init__.py | 10 +- synapse/visibility.py | 14 + tests/rest/admin/test_room.py | 4 +- tests/rest/client/test_rooms.py | 4 +- tests/rest/client/test_sticky_events.py | 179 ++++++++++ tests/rest/client/utils.py | 39 +++ tests/storage/test_sticky_events.py | 278 +++++++++++++++ 30 files changed, 1147 insertions(+), 15 deletions(-) create mode 100644 changelog.d/19365.feature create mode 100644 synapse/storage/databases/main/sticky_events.py create mode 100644 synapse/storage/schema/main/delta/93/01_sticky_events.sql create mode 100644 synapse/storage/schema/main/delta/93/01_sticky_events_seq.sql.postgres create mode 100644 tests/rest/client/test_sticky_events.py create mode 100644 tests/storage/test_sticky_events.py diff --git a/changelog.d/19365.feature b/changelog.d/19365.feature new file mode 100644 index 00000000000..c35afdc179e --- /dev/null +++ b/changelog.d/19365.feature @@ -0,0 +1 @@ +Support sending and receiving [MSC4354 Sticky Event](https://github.com/matrix-org/matrix-spec-proposals/pull/4354) metadata. \ No newline at end of file diff --git a/docker/complement/conf/workers-shared-extra.yaml.j2 b/docker/complement/conf/workers-shared-extra.yaml.j2 index 101ff153a5a..120b3b94962 100644 --- a/docker/complement/conf/workers-shared-extra.yaml.j2 +++ b/docker/complement/conf/workers-shared-extra.yaml.j2 @@ -139,6 +139,8 @@ experimental_features: msc4155_enabled: true # Thread Subscriptions msc4306_enabled: true + # Sticky Events + msc4354_enabled: true server_notices: system_mxid_localpart: _server diff --git a/synapse/api/constants.py b/synapse/api/constants.py index 9b6a68e929c..b8ef5dac50f 100644 --- a/synapse/api/constants.py +++ b/synapse/api/constants.py @@ -24,7 +24,9 @@ """Contains constants from the specification.""" import enum -from typing import Final +from typing import Final, TypedDict + +from synapse.util.duration import Duration # the max size of a (canonical-json-encoded) event MAX_PDU_SIZE = 65536 @@ -292,6 +294,8 @@ class EventUnsignedContentFields: # Requesting user's membership, per MSC4115 MEMBERSHIP: Final = "membership" + STICKY_TTL: Final = "msc4354_sticky_duration_ttl_ms" + class MTextFields: """Fields found inside m.text content blocks.""" @@ -377,3 +381,40 @@ class Direction(enum.Enum): class ProfileFields: DISPLAYNAME: Final = "displayname" AVATAR_URL: Final = "avatar_url" + + +class StickyEventField(TypedDict): + """ + Dict content of the `sticky` part of an event. + """ + + duration_ms: int + + +class StickyEvent: + QUERY_PARAM_NAME: Final = "org.matrix.msc4354.sticky_duration_ms" + """ + Query parameter used by clients for setting the sticky duration of an event they are sending. + + Applies to: + - /rooms/.../send/... + - /rooms/.../state/... + """ + + EVENT_FIELD_NAME: Final = "msc4354_sticky" + """ + Name of the field in the top-level event dict that contains the sticky event dict. + """ + + MAX_DURATION: Duration = Duration(hours=1) + """ + Maximum stickiness duration as specified in MSC4354. + Ensures that data in the /sync response can go down and not grow unbounded. + """ + + MAX_EVENTS_IN_SYNC: Final = 100 + """ + Maximum number of sticky events to include in /sync. + + This is the default specified in the MSC. Chosen arbitrarily. + """ diff --git a/synapse/app/generic_worker.py b/synapse/app/generic_worker.py index 0a4abd18393..159cd44237e 100644 --- a/synapse/app/generic_worker.py +++ b/synapse/app/generic_worker.py @@ -102,6 +102,7 @@ from synapse.storage.databases.main.sliding_sync import SlidingSyncStore from synapse.storage.databases.main.state import StateGroupWorkerStore from synapse.storage.databases.main.stats import StatsStore +from synapse.storage.databases.main.sticky_events import StickyEventsWorkerStore from synapse.storage.databases.main.stream import StreamWorkerStore from synapse.storage.databases.main.tags import TagsWorkerStore from synapse.storage.databases.main.task_scheduler import TaskSchedulerWorkerStore @@ -137,6 +138,7 @@ class GenericWorkerStore( RoomWorkerStore, DirectoryWorkerStore, ThreadSubscriptionsWorkerStore, + StickyEventsWorkerStore, PushRulesWorkerStore, ApplicationServiceTransactionWorkerStore, ApplicationServiceWorkerStore, diff --git a/synapse/config/experimental.py b/synapse/config/experimental.py index 9b6ff482cfd..b6c8b8c0625 100644 --- a/synapse/config/experimental.py +++ b/synapse/config/experimental.py @@ -580,5 +580,11 @@ def read_config( # (and MSC4308: Thread Subscriptions extension to Sliding Sync) self.msc4306_enabled: bool = experimental.get("msc4306_enabled", False) + # MSC4354: Sticky Events + # Tracked in: https://github.com/element-hq/synapse/issues/19409 + # Note that sticky events persisted before this feature is enabled will not be + # considered sticky by the local homeserver. + self.msc4354_enabled: bool = experimental.get("msc4354_enabled", False) + # MSC4380: Invite blocking self.msc4380_enabled: bool = experimental.get("msc4380_enabled", False) diff --git a/synapse/config/workers.py b/synapse/config/workers.py index ec8ab9506bc..996be88cb26 100644 --- a/synapse/config/workers.py +++ b/synapse/config/workers.py @@ -127,7 +127,9 @@ class WriterLocations: """Specifies the instances that write various streams. Attributes: - events: The instances that write to the event and backfill streams. + events: The instances that write to the event, backfill and `sticky_events` streams. + (`sticky_events` is written to during event persistence so must be handled by the + same stream writers.) typing: The instances that write to the typing stream. Currently can only be a single instance. to_device: The instances that write to the to_device stream. Currently diff --git a/synapse/events/__init__.py b/synapse/events/__init__.py index c7eaf7eda2b..e6162997dd7 100644 --- a/synapse/events/__init__.py +++ b/synapse/events/__init__.py @@ -36,7 +36,12 @@ import attr from unpaddedbase64 import encode_base64 -from synapse.api.constants import EventContentFields, EventTypes, RelationTypes +from synapse.api.constants import ( + EventContentFields, + EventTypes, + RelationTypes, + StickyEvent, +) from synapse.api.room_versions import EventFormatVersions, RoomVersion, RoomVersions from synapse.synapse_rust.events import EventInternalMetadata from synapse.types import ( @@ -44,6 +49,7 @@ StrCollection, ) from synapse.util.caches import intern_dict +from synapse.util.duration import Duration from synapse.util.frozenutils import freeze if TYPE_CHECKING: @@ -318,6 +324,28 @@ def freeze(self) -> None: # this will be a no-op if the event dict is already frozen. self._dict = freeze(self._dict) + def sticky_duration(self) -> Duration | None: + """ + Returns the effective sticky duration of this event, or None + if the event does not have a sticky duration. + (Sticky Events are a MSC4354 feature.) + + Clamps the sticky duration to the maximum allowed duration. + """ + sticky_obj = self.get_dict().get(StickyEvent.EVENT_FIELD_NAME, None) + if type(sticky_obj) is not dict: + return None + sticky_duration_ms = sticky_obj.get("duration_ms", None) + # MSC: Clamp to 0 and MAX_DURATION (1 hour) + # We use `type(...) is int` to avoid accepting bools as `isinstance(True, int)` + # (bool is a subclass of int) + if type(sticky_duration_ms) is int and sticky_duration_ms >= 0: + return min( + Duration(milliseconds=sticky_duration_ms), + StickyEvent.MAX_DURATION, + ) + return None + def __str__(self) -> str: return self.__repr__() diff --git a/synapse/events/builder.py b/synapse/events/builder.py index 6a2812109d0..2cd1bf6106f 100644 --- a/synapse/events/builder.py +++ b/synapse/events/builder.py @@ -24,7 +24,7 @@ import attr from signedjson.types import SigningKey -from synapse.api.constants import MAX_DEPTH, EventTypes +from synapse.api.constants import MAX_DEPTH, EventTypes, StickyEvent, StickyEventField from synapse.api.room_versions import ( KNOWN_EVENT_FORMAT_VERSIONS, EventFormatVersions, @@ -89,6 +89,10 @@ class EventBuilder: content: JsonDict = attr.Factory(dict) unsigned: JsonDict = attr.Factory(dict) + sticky: StickyEventField | None = None + """ + Fields for MSC4354: Sticky Events + """ # These only exist on a subset of events, so they raise AttributeError if # someone tries to get them when they don't exist. @@ -269,6 +273,9 @@ async def build( if self._origin_server_ts is not None: event_dict["origin_server_ts"] = self._origin_server_ts + if self.sticky is not None: + event_dict[StickyEvent.EVENT_FIELD_NAME] = self.sticky + return create_local_event_from_event_dict( clock=self._clock, hostname=self._hostname, @@ -318,6 +325,7 @@ def for_room_version( unsigned=key_values.get("unsigned", {}), redacts=key_values.get("redacts", None), origin_server_ts=key_values.get("origin_server_ts", None), + sticky=key_values.get(StickyEvent.EVENT_FIELD_NAME, None), ) diff --git a/synapse/handlers/delayed_events.py b/synapse/handlers/delayed_events.py index c58d1d42bcf..7e41716f1e2 100644 --- a/synapse/handlers/delayed_events.py +++ b/synapse/handlers/delayed_events.py @@ -17,7 +17,7 @@ from twisted.internet.interfaces import IDelayedCall -from synapse.api.constants import EventTypes +from synapse.api.constants import EventTypes, StickyEvent, StickyEventField from synapse.api.errors import ShadowBanError, SynapseError from synapse.api.ratelimiting import Ratelimiter from synapse.config.workers import MAIN_PROCESS_INSTANCE_NAME @@ -333,6 +333,7 @@ async def add( origin_server_ts: int | None, content: JsonDict, delay: int, + sticky_duration_ms: int | None, ) -> str: """ Creates a new delayed event and schedules its delivery. @@ -346,7 +347,9 @@ async def add( If None, the timestamp will be the actual time when the event is sent. content: The content of the event to be sent. delay: How long (in milliseconds) to wait before automatically sending the event. - + sticky_duration_ms: If an MSC4354 sticky event: the sticky duration (in milliseconds). + The event will be attempted to be reliably delivered to clients and remote servers + during its sticky period. Returns: The ID of the added delayed event. Raises: @@ -382,6 +385,7 @@ async def add( origin_server_ts=origin_server_ts, content=content, delay=delay, + sticky_duration_ms=sticky_duration_ms, ) if self._repl_client is not None: @@ -570,7 +574,10 @@ async def _send_event( if event.state_key is not None: event_dict["state_key"] = event.state_key - + if event.sticky_duration_ms is not None: + event_dict[StickyEvent.EVENT_FIELD_NAME] = StickyEventField( + duration_ms=event.sticky_duration_ms + ) ( sent_event, _, diff --git a/synapse/notifier.py b/synapse/notifier.py index cf3923110e3..93d438def71 100644 --- a/synapse/notifier.py +++ b/synapse/notifier.py @@ -526,6 +526,7 @@ def on_new_event( StreamKeyType.TYPING, StreamKeyType.UN_PARTIAL_STATED_ROOMS, StreamKeyType.THREAD_SUBSCRIPTIONS, + StreamKeyType.STICKY_EVENTS, ], new_token: int, users: Collection[str | UserID] | None = None, diff --git a/synapse/replication/tcp/client.py b/synapse/replication/tcp/client.py index fdda932ead2..bc7e46d4c92 100644 --- a/synapse/replication/tcp/client.py +++ b/synapse/replication/tcp/client.py @@ -43,7 +43,10 @@ UnPartialStatedEventStream, UnPartialStatedRoomStream, ) -from synapse.replication.tcp.streams._base import ThreadSubscriptionsStream +from synapse.replication.tcp.streams._base import ( + StickyEventsStream, + ThreadSubscriptionsStream, +) from synapse.replication.tcp.streams.events import ( EventsStream, EventsStreamEventRow, @@ -262,6 +265,12 @@ async def on_rdata( token, users=[row.user_id for row in rows], ) + elif stream_name == StickyEventsStream.NAME: + self.notifier.on_new_event( + StreamKeyType.STICKY_EVENTS, + token, + rooms=[row.room_id for row in rows], + ) await self._presence_handler.process_replication_rows( stream_name, instance_name, token, rows diff --git a/synapse/replication/tcp/handler.py b/synapse/replication/tcp/handler.py index 087c87545eb..ad9fed72dd8 100644 --- a/synapse/replication/tcp/handler.py +++ b/synapse/replication/tcp/handler.py @@ -67,6 +67,7 @@ ) from synapse.replication.tcp.streams._base import ( DeviceListsStream, + StickyEventsStream, ThreadSubscriptionsStream, ) from synapse.util.background_queue import BackgroundQueue @@ -217,6 +218,12 @@ def __init__(self, hs: "HomeServer"): continue + if isinstance(stream, StickyEventsStream): + if hs.get_instance_name() in hs.config.worker.writers.events: + self._streams_to_replicate.append(stream) + + continue + if isinstance(stream, DeviceListsStream): if hs.get_instance_name() in hs.config.worker.writers.device_lists: self._streams_to_replicate.append(stream) diff --git a/synapse/replication/tcp/streams/__init__.py b/synapse/replication/tcp/streams/__init__.py index 87ac0a5ae17..067847617fa 100644 --- a/synapse/replication/tcp/streams/__init__.py +++ b/synapse/replication/tcp/streams/__init__.py @@ -40,6 +40,7 @@ PushersStream, PushRulesStream, ReceiptsStream, + StickyEventsStream, Stream, ThreadSubscriptionsStream, ToDeviceStream, @@ -68,6 +69,7 @@ ToDeviceStream, FederationStream, AccountDataStream, + StickyEventsStream, ThreadSubscriptionsStream, UnPartialStatedRoomStream, UnPartialStatedEventStream, @@ -90,6 +92,7 @@ "ToDeviceStream", "FederationStream", "AccountDataStream", + "StickyEventsStream", "ThreadSubscriptionsStream", "UnPartialStatedRoomStream", "UnPartialStatedEventStream", diff --git a/synapse/replication/tcp/streams/_base.py b/synapse/replication/tcp/streams/_base.py index 4fb2aac2029..1ea6b4fa857 100644 --- a/synapse/replication/tcp/streams/_base.py +++ b/synapse/replication/tcp/streams/_base.py @@ -763,3 +763,48 @@ async def _update_function( return [], to_token, False return rows, rows[-1][0], len(updates) == limit + + +@attr.s(slots=True, auto_attribs=True) +class StickyEventsStreamRow: + """Stream to inform workers about changes to sticky events.""" + + room_id: str + + event_id: str + """The sticky event ID""" + + +class StickyEventsStream(_StreamFromIdGen): + """A sticky event was changed.""" + + NAME = "sticky_events" + ROW_TYPE = StickyEventsStreamRow + + def __init__(self, hs: "HomeServer"): + self.store = hs.get_datastores().main + super().__init__( + hs.get_instance_name(), + self._update_function, + self.store._sticky_events_id_gen, + ) + + async def _update_function( + self, instance_name: str, from_token: int, to_token: int, limit: int + ) -> StreamUpdateResult: + updates = await self.store.get_updated_sticky_events( + from_id=from_token, to_id=to_token, limit=limit + ) + rows = [ + ( + update.stream_id, + # These are the args to `StickyEventsStreamRow` + (update.room_id, update.event_id), + ) + for update in updates + ] + + if not rows: + return [], to_token, False + + return rows, rows[-1][0], len(updates) == limit diff --git a/synapse/rest/client/room.py b/synapse/rest/client/room.py index 5e7dcb01911..9172bfcb4ed 100644 --- a/synapse/rest/client/room.py +++ b/synapse/rest/client/room.py @@ -34,7 +34,13 @@ from twisted.web.server import Request from synapse import event_auth -from synapse.api.constants import Direction, EventTypes, Membership +from synapse.api.constants import ( + Direction, + EventTypes, + Membership, + StickyEvent, + StickyEventField, +) from synapse.api.errors import ( AuthError, Codes, @@ -210,6 +216,7 @@ def __init__(self, hs: "HomeServer"): self.clock = hs.get_clock() self._max_event_delay_ms = hs.config.server.max_event_delay_ms self._spam_checker_module_callbacks = hs.get_module_api_callbacks().spam_checker + self._msc4354_enabled = hs.config.experimental.msc4354_enabled def register(self, http_server: HttpServer) -> None: # /rooms/$roomid/state/$eventtype @@ -331,6 +338,10 @@ async def on_PUT( if requester.app_service: origin_server_ts = parse_integer(request, "ts") + sticky_duration_ms: int | None = None + if self._msc4354_enabled: + sticky_duration_ms = parse_integer(request, StickyEvent.QUERY_PARAM_NAME) + delay = _parse_request_delay(request, self._max_event_delay_ms) if delay is not None: delay_id = await self.delayed_events_handler.add( @@ -341,6 +352,7 @@ async def on_PUT( origin_server_ts=origin_server_ts, content=content, delay=delay, + sticky_duration_ms=sticky_duration_ms, ) set_tag("delay_id", delay_id) @@ -368,6 +380,10 @@ async def on_PUT( "room_id": room_id, "sender": requester.user.to_string(), } + if sticky_duration_ms is not None: + event_dict[StickyEvent.EVENT_FIELD_NAME] = StickyEventField( + duration_ms=sticky_duration_ms + ) if state_key is not None: event_dict["state_key"] = state_key @@ -400,6 +416,7 @@ def __init__(self, hs: "HomeServer"): self.delayed_events_handler = hs.get_delayed_events_handler() self.auth = hs.get_auth() self._max_event_delay_ms = hs.config.server.max_event_delay_ms + self._msc4354_enabled = hs.config.experimental.msc4354_enabled def register(self, http_server: HttpServer) -> None: # /rooms/$roomid/send/$event_type[/$txn_id] @@ -420,6 +437,10 @@ async def _do( if requester.app_service: origin_server_ts = parse_integer(request, "ts") + sticky_duration_ms: int | None = None + if self._msc4354_enabled: + sticky_duration_ms = parse_integer(request, StickyEvent.QUERY_PARAM_NAME) + delay = _parse_request_delay(request, self._max_event_delay_ms) if delay is not None: delay_id = await self.delayed_events_handler.add( @@ -430,6 +451,7 @@ async def _do( origin_server_ts=origin_server_ts, content=content, delay=delay, + sticky_duration_ms=sticky_duration_ms, ) set_tag("delay_id", delay_id) @@ -446,6 +468,11 @@ async def _do( if origin_server_ts is not None: event_dict["origin_server_ts"] = origin_server_ts + if sticky_duration_ms is not None: + event_dict[StickyEvent.EVENT_FIELD_NAME] = StickyEventField( + duration_ms=sticky_duration_ms + ) + try: ( event, diff --git a/synapse/rest/client/versions.py b/synapse/rest/client/versions.py index 75f27c98dea..89458495311 100644 --- a/synapse/rest/client/versions.py +++ b/synapse/rest/client/versions.py @@ -182,6 +182,8 @@ async def on_GET(self, request: SynapseRequest) -> tuple[int, JsonDict]: "org.matrix.msc4306": self.config.experimental.msc4306_enabled, # MSC4169: Backwards-compatible redaction sending using `/send` "com.beeper.msc4169": self.config.experimental.msc4169_enabled, + # MSC4354: Sticky events + "org.matrix.msc4354": self.config.experimental.msc4354_enabled, # MSC4380: Invite blocking "org.matrix.msc4380": self.config.experimental.msc4380_enabled, }, diff --git a/synapse/storage/databases/main/__init__.py b/synapse/storage/databases/main/__init__.py index 12593094f18..9f8d4debbe0 100644 --- a/synapse/storage/databases/main/__init__.py +++ b/synapse/storage/databases/main/__init__.py @@ -34,6 +34,7 @@ ) from synapse.storage.databases.main.sliding_sync import SlidingSyncStore from synapse.storage.databases.main.stats import UserSortOrder +from synapse.storage.databases.main.sticky_events import StickyEventsWorkerStore from synapse.storage.databases.main.thread_subscriptions import ( ThreadSubscriptionsWorkerStore, ) @@ -144,6 +145,7 @@ class DataStore( TagsStore, AccountDataStore, ThreadSubscriptionsWorkerStore, + StickyEventsWorkerStore, PushRulesWorkerStore, StreamWorkerStore, OpenIdStore, diff --git a/synapse/storage/databases/main/delayed_events.py b/synapse/storage/databases/main/delayed_events.py index 55471505154..1727f589e2a 100644 --- a/synapse/storage/databases/main/delayed_events.py +++ b/synapse/storage/databases/main/delayed_events.py @@ -54,6 +54,7 @@ class EventDetails: origin_server_ts: Timestamp | None content: JsonDict device_id: DeviceID | None + sticky_duration_ms: int | None @attr.s(slots=True, frozen=True, auto_attribs=True) @@ -122,6 +123,7 @@ async def add_delayed_event( origin_server_ts: int | None, content: JsonDict, delay: int, + sticky_duration_ms: int | None, ) -> tuple[DelayID, Timestamp]: """ Inserts a new delayed event in the DB. @@ -148,6 +150,7 @@ def add_delayed_event_txn(txn: LoggingTransaction) -> Timestamp: "state_key": state_key, "origin_server_ts": origin_server_ts, "content": json_encoder.encode(content), + "sticky_duration_ms": sticky_duration_ms, }, ) @@ -299,6 +302,7 @@ def process_timeout_delayed_events_txn( "send_ts", "content", "device_id", + "sticky_duration_ms", ) ) sql_update = "UPDATE delayed_events SET is_processed = TRUE" @@ -344,6 +348,7 @@ def process_timeout_delayed_events_txn( Timestamp(row[5] if row[5] is not None else row[6]), db_to_json(row[7]), DeviceID(row[8]) if row[8] is not None else None, + int(row[9]) if row[9] is not None else None, DelayID(row[0]), UserLocalpart(row[1]), ) @@ -392,6 +397,7 @@ def process_target_delayed_event_txn( origin_server_ts, content, device_id, + sticky_duration_ms, user_localpart """, (delay_id,), @@ -407,8 +413,9 @@ def process_target_delayed_event_txn( Timestamp(row[3]) if row[3] is not None else None, db_to_json(row[4]), DeviceID(row[5]) if row[5] is not None else None, + int(row[6]) if row[6] is not None else None, DelayID(delay_id), - UserLocalpart(row[6]), + UserLocalpart(row[7]), ) return event, self._get_next_delayed_event_send_ts_txn(txn) diff --git a/synapse/storage/databases/main/events.py b/synapse/storage/databases/main/events.py index 60fc884c3a6..cb452dbc9b1 100644 --- a/synapse/storage/databases/main/events.py +++ b/synapse/storage/databases/main/events.py @@ -264,6 +264,7 @@ def __init__( self.database_engine = db.engine self._clock = hs.get_clock() self._instance_name = hs.get_instance_name() + self._msc4354_enabled = hs.config.experimental.msc4354_enabled self._ephemeral_messages_enabled = hs.config.server.enable_ephemeral_messages self.is_mine_id = hs.is_mine_id @@ -1185,6 +1186,11 @@ def _persist_events_txn( sliding_sync_table_changes, ) + if self._msc4354_enabled: + self.store.insert_sticky_events_txn( + txn, [ev for ev, _ in events_and_contexts] + ) + # We only update the sliding sync tables for non-backfilled events. self._update_sliding_sync_tables_with_new_persisted_events_txn( txn, room_id, events_and_contexts @@ -2646,6 +2652,11 @@ def _update_outliers_txn( # event isn't an outlier any more. self._update_backward_extremeties(txn, [event]) + if self._msc4354_enabled and event.sticky_duration(): + # The de-outliered event is sticky. Update the sticky events table to ensure + # we deliver this down /sync. + self.store.insert_sticky_events_txn(txn, [event]) + return [ec for ec in events_and_contexts if ec[0] not in to_remove] def _store_event_txn( diff --git a/synapse/storage/databases/main/sticky_events.py b/synapse/storage/databases/main/sticky_events.py new file mode 100644 index 00000000000..101306296e4 --- /dev/null +++ b/synapse/storage/databases/main/sticky_events.py @@ -0,0 +1,322 @@ +# +# This file is licensed under the Affero General Public License (AGPL) version 3. +# +# Copyright (C) 2025 New Vector, Ltd +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# See the GNU Affero General Public License for more details: +# . +import logging +import random +from dataclasses import dataclass +from typing import ( + TYPE_CHECKING, +) + +from twisted.internet.defer import Deferred + +from synapse.events import EventBase +from synapse.replication.tcp.streams._base import StickyEventsStream +from synapse.storage.database import ( + DatabasePool, + LoggingDatabaseConnection, + LoggingTransaction, +) +from synapse.storage.databases.main.cache import CacheInvalidationWorkerStore +from synapse.storage.databases.main.state import StateGroupWorkerStore +from synapse.storage.engines import PostgresEngine, Sqlite3Engine +from synapse.storage.util.id_generators import MultiWriterIdGenerator +from synapse.util.duration import Duration + +if TYPE_CHECKING: + from synapse.server import HomeServer + +logger = logging.getLogger(__name__) + +DELETE_EXPIRED_STICKY_EVENTS_INTERVAL = Duration(hours=1) +""" +Remove entries from the sticky_events table at this frequency. +Note: don't be misled, we still honour shorter expiration timeouts, +because readers of the sticky_events table filter out expired sticky events +themselves, even if they aren't deleted from the table yet. + +Currently just an arbitrary choice. +Frequent enough to clean up expired sticky events promptly, +especially given the short cap on the lifetime of sticky events. +""" + + +@dataclass(frozen=True) +class StickyEventUpdate: + stream_id: int + room_id: str + event_id: str + soft_failed: bool + + +class StickyEventsWorkerStore(StateGroupWorkerStore, CacheInvalidationWorkerStore): + def __init__( + self, + database: DatabasePool, + db_conn: LoggingDatabaseConnection, + hs: "HomeServer", + ): + super().__init__(database, db_conn, hs) + + self._can_write_to_sticky_events = ( + self._instance_name in hs.config.worker.writers.events + ) + + # Technically this means we will cleanup N times, once per event persister, maybe put on master? + if self._can_write_to_sticky_events: + # Start a looping call to clean up the `sticky_events` table + # + # Because this will run once per event persister (for now), + # randomly stagger the initial time so that they don't all + # coincide with each other if the workers are deployed at the + # same time. This allows each cleanup to be somewhat more effective + # than if they all started at the same time, as they would all be + # cleaning up the same thing whereas each worker gets to clean up a little + # throughout the hour when they're staggered. + # + # Concurrent execution of the same deletions could also lead to + # repeatable serialisation violations in the database transaction, + # meaning we'd have to retry the transaction several times. + # + # This staggering is not critical, it's just best-effort. + self.clock.call_later( + # random() is 0.0 to 1.0 + DELETE_EXPIRED_STICKY_EVENTS_INTERVAL * random.random(), + self.clock.looping_call, + self._run_background_cleanup, + DELETE_EXPIRED_STICKY_EVENTS_INTERVAL, + ) + + self._sticky_events_id_gen: MultiWriterIdGenerator = MultiWriterIdGenerator( + db_conn=db_conn, + db=database, + notifier=hs.get_replication_notifier(), + stream_name="sticky_events", + server_name=self.server_name, + instance_name=self._instance_name, + tables=[ + ("sticky_events", "instance_name", "stream_id"), + ], + sequence_name="sticky_events_sequence", + writers=hs.config.worker.writers.events, + ) + + if hs.config.experimental.msc4354_enabled and isinstance( + self.database_engine, Sqlite3Engine + ): + import sqlite3 + + if sqlite3.sqlite_version_info < (3, 40, 0): + raise RuntimeError( + f"Experimental MSC4354 Sticky Events enabled but SQLite3 version is too old: {sqlite3.sqlite_version_info}, must be at least 3.40. Disable MSC4354 Sticky Events, switch to Postgres, or upgrade SQLite. See https://github.com/element-hq/synapse/issues/19428" + ) + + def process_replication_position( + self, stream_name: str, instance_name: str, token: int + ) -> None: + if stream_name == StickyEventsStream.NAME: + self._sticky_events_id_gen.advance(instance_name, token) + super().process_replication_position(stream_name, instance_name, token) + + def get_max_sticky_events_stream_id(self) -> int: + """Get the current maximum stream_id for thread subscriptions. + + Returns: + The maximum stream_id + """ + return self._sticky_events_id_gen.get_current_token() + + def get_sticky_events_stream_id_generator(self) -> MultiWriterIdGenerator: + return self._sticky_events_id_gen + + async def get_updated_sticky_events( + self, *, from_id: int, to_id: int, limit: int + ) -> list[StickyEventUpdate]: + """Get updates to sticky events between two stream IDs. + + Bounds: from_id < ... <= to_id + + Args: + from_id: The starting stream ID (exclusive) + to_id: The ending stream ID (inclusive) + limit: The maximum number of rows to return + + Returns: + list of StickyEventUpdate update rows + """ + + if not self.hs.config.experimental.msc4354_enabled: + # We need to prevent `_get_updated_sticky_events_txn` + # from running when MSC4354 is turned off, because the query used + # for SQLite is not compatible with Ubuntu 22.04 (as used in our CI olddeps run). + # It's technically out of support. + # See: https://github.com/element-hq/synapse/issues/19428 + return [] + + return await self.db_pool.runInteraction( + "get_updated_sticky_events", + self._get_updated_sticky_events_txn, + from_id, + to_id, + limit, + ) + + def _get_updated_sticky_events_txn( + self, txn: LoggingTransaction, from_id: int, to_id: int, limit: int + ) -> list[StickyEventUpdate]: + if isinstance(self.database_engine, PostgresEngine): + expr_soft_failed = "COALESCE(((ej.internal_metadata::jsonb)->>'soft_failed')::boolean, FALSE)" + else: + expr_soft_failed = "COALESCE(ej.internal_metadata->>'soft_failed', FALSE)" + + txn.execute( + f""" + SELECT se.stream_id, se.room_id, se.event_id, + {expr_soft_failed} AS "soft_failed" + FROM sticky_events se + INNER JOIN event_json ej USING (event_id) + WHERE ? < stream_id AND stream_id <= ? + LIMIT ? + """, + (from_id, to_id, limit), + ) + + return [ + StickyEventUpdate( + stream_id=stream_id, + room_id=room_id, + event_id=event_id, + soft_failed=bool(soft_failed), + ) + for stream_id, room_id, event_id, soft_failed in txn + ] + + def insert_sticky_events_txn( + self, + txn: LoggingTransaction, + events: list[EventBase], + ) -> None: + """ + Insert events into the sticky_events table. + + Skips inserting events: + - if they are considered spammy by the policy server; + (unsure if correct, track: https://github.com/matrix-org/matrix-spec-proposals/pull/4354#discussion_r2727593350) + - if they are rejected; + - if they are outliers (they should be reconsidered for insertion when de-outliered); or + - if they are not sticky (e.g. if the stickiness expired). + + Skipping the insertion of these types of 'invalid' events is useful for performance reasons because + they would fill up the table yet we wouldn't show them to clients anyway. + + Since syncing clients can't (easily?) 'skip over' sticky events (due to being in-order, reliably delivered), + tracking loads of invalid events in the table could make it expensive for servers to retrieve the sticky events that are actually valid. + + For instance, someone spamming 1000s of rejected or 'policy_server_spammy' events could clog up this table in a way that means we either + have to deliver empty payloads to syncing clients, or consider substantially more than 100 events in order to gather a 100-sized batch to send down. + """ + + now_ms = self.clock.time_msec() + # event, expires_at + sticky_events: list[tuple[EventBase, int]] = [] + for ev in events: + # MSC: Note: policy servers and other similar antispam techniques still apply to these events. + if ev.internal_metadata.policy_server_spammy: + continue + # We shouldn't be passed rejected events, but if we do, we filter them out too. + if ev.rejected_reason is not None: + continue + # We can't persist outlier sticky events as we don't know the room state at that event + if ev.internal_metadata.is_outlier(): + continue + sticky_duration = ev.sticky_duration() + if sticky_duration is None: + continue + # Calculate the end time as start_time + effecitve sticky duration + expires_at = min(ev.origin_server_ts, now_ms) + sticky_duration.as_millis() + # Filter out already expired sticky events + if expires_at <= now_ms: + continue + + sticky_events.append((ev, expires_at)) + + if len(sticky_events) == 0: + return + + logger.info( + "inserting %d sticky events in room %s", + len(sticky_events), + sticky_events[0][0].room_id, + ) + + # Generate stream_ids in one go + sticky_events_with_ids = zip( + sticky_events, + self._sticky_events_id_gen.get_next_mult_txn(txn, len(sticky_events)), + strict=True, + ) + + self.db_pool.simple_insert_many_txn( + txn, + "sticky_events", + keys=( + "instance_name", + "stream_id", + "room_id", + "event_id", + "event_stream_ordering", + "sender", + "expires_at", + ), + values=[ + ( + self._instance_name, + stream_id, + ev.room_id, + ev.event_id, + ev.internal_metadata.stream_ordering, + ev.sender, + expires_at, + ) + for (ev, expires_at), stream_id in sticky_events_with_ids + ], + ) + + async def _delete_expired_sticky_events(self) -> None: + await self.db_pool.runInteraction( + "_delete_expired_sticky_events", + self._delete_expired_sticky_events_txn, + self.clock.time_msec(), + ) + + def _delete_expired_sticky_events_txn( + self, txn: LoggingTransaction, now: int + ) -> None: + """ + From the `sticky_events` table, deletes all entries whose expiry is in the past + (older than `now`). + + This is fine because we don't consider the events as sticky anymore when that's + happened. + """ + txn.execute( + """ + DELETE FROM sticky_events WHERE expires_at < ? + """, + (now,), + ) + + def _run_background_cleanup(self) -> Deferred: + return self.hs.run_as_background_process( + "delete_expired_sticky_events", + self._delete_expired_sticky_events, + ) diff --git a/synapse/storage/schema/main/delta/93/01_sticky_events.sql b/synapse/storage/schema/main/delta/93/01_sticky_events.sql new file mode 100644 index 00000000000..59fded5959a --- /dev/null +++ b/synapse/storage/schema/main/delta/93/01_sticky_events.sql @@ -0,0 +1,60 @@ +-- +-- This file is licensed under the Affero General Public License (AGPL) version 3. +-- +-- Copyright (C) 2025 New Vector, Ltd +-- +-- This program is free software: you can redistribute it and/or modify +-- it under the terms of the GNU Affero General Public License as +-- published by the Free Software Foundation, either version 3 of the +-- License, or (at your option) any later version. +-- +-- See the GNU Affero General Public License for more details: +-- . + +-- Tracks sticky events. +-- Excludes 'policy_server_spammy' events, outliers, rejected events. +-- +-- Skipping the insertion of these types of 'invalid' events is useful for performance reasons because +-- they would fill up the table yet we wouldn't show them to clients anyway. +-- +-- Since syncing clients can't (easily) 'skip over' sticky events (due to being in-order, reliably delivered), +-- tracking loads of invalid events in the table could make it expensive for servers to retrieve the sticky events that are actually valid. +-- +-- For instance, someone spamming 1000s of rejected or 'policy_server_spammy' events could clog up this table in a way that means we either +-- have to deliver empty payloads to syncing clients, or consider substantially more than 100 events in order to gather a 100-sized batch to send down. +-- +-- May contain sticky events that have expired since being inserted, +-- although they will be periodically cleaned up in the background. +CREATE TABLE sticky_events ( + -- Position in the sticky events stream + stream_id INTEGER NOT NULL PRIMARY KEY, + + -- Name of the worker sending this. (This makes the stream compatible with multiple writers.) + instance_name TEXT NOT NULL, + + -- The event ID of the sticky event itself. + event_id TEXT NOT NULL, + + -- The room ID that the sticky event is in. + -- Denormalised for performance. (Safe as it's an immutable property of the event.) + room_id TEXT NOT NULL, + + -- The stream_ordering of the event. + -- Denormalised for performance since we will want to sort these by stream_ordering + -- when fetching them. (Safe as it's an immutable property of the event.) + event_stream_ordering INTEGER NOT NULL UNIQUE, + + -- Sender of the sticky event. + -- Denormalised for performance so we can query only for sticky events originating + -- from our homeserver. (Safe as it's an immutable property of the event.) + sender TEXT NOT NULL, + + -- When the sticky event expires, in milliseconds since the Unix epoch. + expires_at BIGINT NOT NULL +); + +-- For pulling out sticky events by room at send time, obeying stream ordering range limits. +CREATE INDEX sticky_events_room_idx ON sticky_events (room_id, event_stream_ordering); + +-- A optional integer for combining sticky events with delayed events. Used at send time. +ALTER TABLE delayed_events ADD COLUMN sticky_duration_ms BIGINT; diff --git a/synapse/storage/schema/main/delta/93/01_sticky_events_seq.sql.postgres b/synapse/storage/schema/main/delta/93/01_sticky_events_seq.sql.postgres new file mode 100644 index 00000000000..9ba72856bc9 --- /dev/null +++ b/synapse/storage/schema/main/delta/93/01_sticky_events_seq.sql.postgres @@ -0,0 +1,18 @@ +-- +-- This file is licensed under the Affero General Public License (AGPL) version 3. +-- +-- Copyright (C) 2025 New Vector, Ltd +-- +-- This program is free software: you can redistribute it and/or modify +-- it under the terms of the GNU Affero General Public License as +-- published by the Free Software Foundation, either version 3 of the +-- License, or (at your option) any later version. +-- +-- See the GNU Affero General Public License for more details: +-- . + +CREATE SEQUENCE sticky_events_sequence; +-- Synapse streams start at 2, because the default position is 1 +-- so any item inserted at position 1 is ignored. +-- We have to use nextval not START WITH 2, see https://github.com/element-hq/synapse/issues/18712 +SELECT nextval('sticky_events_sequence'); diff --git a/synapse/streams/events.py b/synapse/streams/events.py index 143f659499b..d2720fb9592 100644 --- a/synapse/streams/events.py +++ b/synapse/streams/events.py @@ -84,6 +84,7 @@ def get_current_token(self) -> StreamToken: self._instance_name ) thread_subscriptions_key = self.store.get_max_thread_subscriptions_stream_id() + sticky_events_key = self.store.get_max_sticky_events_stream_id() token = StreamToken( room_key=self.sources.room.get_current_key(), @@ -98,6 +99,7 @@ def get_current_token(self) -> StreamToken: groups_key=0, un_partial_stated_rooms_key=un_partial_stated_rooms_key, thread_subscriptions_key=thread_subscriptions_key, + sticky_events_key=sticky_events_key, ) return token @@ -125,6 +127,7 @@ async def bound_future_token(self, token: StreamToken) -> StreamToken: StreamKeyType.DEVICE_LIST: self.store.get_device_stream_id_generator(), StreamKeyType.UN_PARTIAL_STATED_ROOMS: self.store.get_un_partial_stated_rooms_id_generator(), StreamKeyType.THREAD_SUBSCRIPTIONS: self.store.get_thread_subscriptions_stream_id_generator(), + StreamKeyType.STICKY_EVENTS: self.store.get_sticky_events_stream_id_generator(), } for _, key in StreamKeyType.__members__.items(): diff --git a/synapse/types/__init__.py b/synapse/types/__init__.py index 99eefb8acba..fb1f1192b70 100644 --- a/synapse/types/__init__.py +++ b/synapse/types/__init__.py @@ -1006,6 +1006,7 @@ class StreamKeyType(Enum): DEVICE_LIST = "device_list_key" UN_PARTIAL_STATED_ROOMS = "un_partial_stated_rooms_key" THREAD_SUBSCRIPTIONS = "thread_subscriptions_key" + STICKY_EVENTS = "sticky_events_key" @attr.s(slots=True, frozen=True, auto_attribs=True) @@ -1027,6 +1028,7 @@ class StreamToken: 9. `groups_key`: `1` (note that this key is now unused) 10. `un_partial_stated_rooms_key`: `379` 11. `thread_subscriptions_key`: 4242 + 12. `sticky_events_key`: 4141 You can see how many of these keys correspond to the various fields in a "/sync" response: @@ -1086,6 +1088,7 @@ class StreamToken: groups_key: int un_partial_stated_rooms_key: int thread_subscriptions_key: int + sticky_events_key: int _SEPARATOR = "_" START: ClassVar["StreamToken"] @@ -1114,6 +1117,7 @@ async def from_string(cls, store: "DataStore", string: str) -> "StreamToken": groups_key, un_partial_stated_rooms_key, thread_subscriptions_key, + sticky_events_key, ) = keys return cls( @@ -1130,6 +1134,7 @@ async def from_string(cls, store: "DataStore", string: str) -> "StreamToken": groups_key=int(groups_key), un_partial_stated_rooms_key=int(un_partial_stated_rooms_key), thread_subscriptions_key=int(thread_subscriptions_key), + sticky_events_key=int(sticky_events_key), ) except CancelledError: raise @@ -1153,6 +1158,7 @@ async def to_string(self, store: "DataStore") -> str: str(self.groups_key), str(self.un_partial_stated_rooms_key), str(self.thread_subscriptions_key), + str(self.sticky_events_key), ] ) @@ -1218,6 +1224,7 @@ def get_field( StreamKeyType.TYPING, StreamKeyType.UN_PARTIAL_STATED_ROOMS, StreamKeyType.THREAD_SUBSCRIPTIONS, + StreamKeyType.STICKY_EVENTS, ], ) -> int: ... @@ -1274,7 +1281,7 @@ def __str__(self) -> str: f"account_data: {self.account_data_key}, push_rules: {self.push_rules_key}, " f"to_device: {self.to_device_key}, device_list: {self.device_list_key}, " f"groups: {self.groups_key}, un_partial_stated_rooms: {self.un_partial_stated_rooms_key}," - f"thread_subscriptions: {self.thread_subscriptions_key})" + f"thread_subscriptions: {self.thread_subscriptions_key}, sticky_events: {self.sticky_events_key})" ) @@ -1290,6 +1297,7 @@ def __str__(self) -> str: groups_key=0, un_partial_stated_rooms_key=0, thread_subscriptions_key=0, + sticky_events_key=0, ) diff --git a/synapse/visibility.py b/synapse/visibility.py index 452a2d50fbb..5ba2a14a24a 100644 --- a/synapse/visibility.py +++ b/synapse/visibility.py @@ -237,6 +237,20 @@ def allowed(event: EventBase) -> EventBase | None: # to the cache! cloned = clone_event(filtered) cloned.unsigned[EventUnsignedContentFields.MEMBERSHIP] = user_membership + if storage.main.config.experimental.msc4354_enabled: + sticky_duration = cloned.sticky_duration() + if sticky_duration: + now_ms = storage.main.clock.time_msec() + expires_at = ( + # min() ensures that the origin server can't lie about the time and + # send the event 'in the future', as that would allow them to exceed + # the 1 hour limit on stickiness duration. + min(cloned.origin_server_ts, now_ms) + sticky_duration.as_millis() + ) + if expires_at > now_ms: + cloned.unsigned[EventUnsignedContentFields.STICKY_TTL] = ( + expires_at - now_ms + ) return cloned diff --git a/tests/rest/admin/test_room.py b/tests/rest/admin/test_room.py index 4070bcaeaa6..b32665eb73b 100644 --- a/tests/rest/admin/test_room.py +++ b/tests/rest/admin/test_room.py @@ -2545,7 +2545,7 @@ def test_timestamp_to_event(self) -> None: def test_topo_token_is_accepted(self) -> None: """Test Topo Token is accepted.""" - token = "t1-0_0_0_0_0_0_0_0_0_0_0" + token = "t1-0_0_0_0_0_0_0_0_0_0_0_0" channel = self.make_request( "GET", "/_synapse/admin/v1/rooms/%s/messages?from=%s" % (self.room_id, token), @@ -2559,7 +2559,7 @@ def test_topo_token_is_accepted(self) -> None: def test_stream_token_is_accepted_for_fwd_pagianation(self) -> None: """Test that stream token is accepted for forward pagination.""" - token = "s0_0_0_0_0_0_0_0_0_0_0" + token = "s0_0_0_0_0_0_0_0_0_0_0_0" channel = self.make_request( "GET", "/_synapse/admin/v1/rooms/%s/messages?from=%s" % (self.room_id, token), diff --git a/tests/rest/client/test_rooms.py b/tests/rest/client/test_rooms.py index 926560afd6b..f85c9939ce4 100644 --- a/tests/rest/client/test_rooms.py +++ b/tests/rest/client/test_rooms.py @@ -2245,7 +2245,7 @@ def prepare(self, reactor: MemoryReactor, clock: Clock, hs: HomeServer) -> None: self.room_id = self.helper.create_room_as(self.user_id) def test_topo_token_is_accepted(self) -> None: - token = "t1-0_0_0_0_0_0_0_0_0_0_0" + token = "t1-0_0_0_0_0_0_0_0_0_0_0_0" channel = self.make_request( "GET", "/rooms/%s/messages?access_token=x&from=%s" % (self.room_id, token) ) @@ -2256,7 +2256,7 @@ def test_topo_token_is_accepted(self) -> None: self.assertTrue("end" in channel.json_body) def test_stream_token_is_accepted_for_fwd_pagianation(self) -> None: - token = "s0_0_0_0_0_0_0_0_0_0_0" + token = "s0_0_0_0_0_0_0_0_0_0_0_0" channel = self.make_request( "GET", "/rooms/%s/messages?access_token=x&from=%s" % (self.room_id, token) ) diff --git a/tests/rest/client/test_sticky_events.py b/tests/rest/client/test_sticky_events.py new file mode 100644 index 00000000000..a6e704fe8c2 --- /dev/null +++ b/tests/rest/client/test_sticky_events.py @@ -0,0 +1,179 @@ +# +# This file is licensed under the Affero General Public License (AGPL) version 3. +# +# Copyright (C) 2025 New Vector, Ltd +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# See the GNU Affero General Public License for more details: +# . +# +# + +import sqlite3 + +from twisted.internet.testing import MemoryReactor + +from synapse.api.constants import EventTypes, EventUnsignedContentFields +from synapse.rest import admin +from synapse.rest.client import login, register, room +from synapse.server import HomeServer +from synapse.types import JsonDict +from synapse.util.clock import Clock +from synapse.util.duration import Duration + +from tests import unittest +from tests.utils import USE_POSTGRES_FOR_TESTS + + +class StickyEventsClientTestCase(unittest.HomeserverTestCase): + """ + Tests for the client-server API parts of MSC4354: Sticky Events + """ + + if not USE_POSTGRES_FOR_TESTS and sqlite3.sqlite_version_info < (3, 40, 0): + # We need the JSON functionality in SQLite + skip = f"SQLite version is too old to support sticky events: {sqlite3.sqlite_version_info} (See https://github.com/element-hq/synapse/issues/19428)" + + servlets = [ + room.register_servlets, + login.register_servlets, + register.register_servlets, + admin.register_servlets, + ] + + def default_config(self) -> JsonDict: + config = super().default_config() + config["experimental_features"] = {"msc4354_enabled": True} + return config + + def prepare(self, reactor: MemoryReactor, clock: Clock, hs: HomeServer) -> None: + # Register an account + self.user_id = self.register_user("user1", "pass") + self.token = self.login(self.user_id, "pass") + + # Create a room + self.room_id = self.helper.create_room_as(self.user_id, tok=self.token) + + def _assert_event_sticky_for(self, event_id: str, sticky_ttl: int) -> None: + channel = self.make_request( + "GET", + f"/rooms/{self.room_id}/event/{event_id}", + access_token=self.token, + ) + + self.assertEqual( + channel.code, 200, f"could not retrieve event {event_id}: {channel.result}" + ) + event = channel.json_body + + self.assertIn( + EventUnsignedContentFields.STICKY_TTL, + event["unsigned"], + f"No {EventUnsignedContentFields.STICKY_TTL} field in {event_id}; event not sticky: {event}", + ) + self.assertEqual( + event["unsigned"][EventUnsignedContentFields.STICKY_TTL], + sticky_ttl, + f"{event_id} had an unexpected sticky TTL: {event}", + ) + + def _assert_event_not_sticky(self, event_id: str) -> None: + channel = self.make_request( + "GET", + f"/rooms/{self.room_id}/event/{event_id}", + access_token=self.token, + ) + + self.assertEqual( + channel.code, 200, f"could not retrieve event {event_id}: {channel.result}" + ) + event = channel.json_body + + self.assertNotIn( + EventUnsignedContentFields.STICKY_TTL, + event["unsigned"], + f"{EventUnsignedContentFields.STICKY_TTL} field unexpectedly found in {event_id}: {event}", + ) + + def test_sticky_event_via_event_endpoint(self) -> None: + # Arrange: Send a sticky event with a specific duration + sticky_event_response = self.helper.send_sticky_event( + self.room_id, + EventTypes.Message, + duration=Duration(minutes=1), + content={"body": "sticky message", "msgtype": "m.text"}, + tok=self.token, + ) + event_id = sticky_event_response["event_id"] + + # If we request the event immediately, it will still have + # 1 minute of stickiness + # The other 100 ms is advanced in FakeChannel.await_result. + self._assert_event_sticky_for(event_id, 59_900) + + # But if we advance time by 59.799 seconds... + # we will get the event on its last millisecond of stickiness + # The other 100 ms is advanced in FakeChannel.await_result. + self.reactor.advance(59.799) + self._assert_event_sticky_for(event_id, 1) + + # Advancing time any more, the event is no longer sticky + self.reactor.advance(0.001) + self._assert_event_not_sticky(event_id) + + +class StickyEventsDisabledClientTestCase(unittest.HomeserverTestCase): + """ + Tests client-facing behaviour of MSC4354: Sticky Events when the feature is + disabled. + """ + + servlets = [ + room.register_servlets, + login.register_servlets, + register.register_servlets, + admin.register_servlets, + ] + + def prepare(self, reactor: MemoryReactor, clock: Clock, hs: HomeServer) -> None: + # Register an account + self.user_id = self.register_user("user1", "pass") + self.token = self.login(self.user_id, "pass") + + # Create a room + self.room_id = self.helper.create_room_as(self.user_id, tok=self.token) + + def _assert_event_not_sticky(self, event_id: str) -> None: + channel = self.make_request( + "GET", + f"/rooms/{self.room_id}/event/{event_id}", + access_token=self.token, + ) + + self.assertEqual( + channel.code, 200, f"could not retrieve event {event_id}: {channel.result}" + ) + event = channel.json_body + + self.assertNotIn( + EventUnsignedContentFields.STICKY_TTL, + event["unsigned"], + f"{EventUnsignedContentFields.STICKY_TTL} field unexpectedly found in {event_id}: {event}", + ) + + def test_sticky_event_via_event_endpoint(self) -> None: + sticky_event_response = self.helper.send_sticky_event( + self.room_id, + EventTypes.Message, + duration=Duration(minutes=1), + content={"body": "sticky message", "msgtype": "m.text"}, + tok=self.token, + ) + event_id = sticky_event_response["event_id"] + + # Since the feature is disabled, the event isn't sticky + self._assert_event_not_sticky(event_id) diff --git a/tests/rest/client/utils.py b/tests/rest/client/utils.py index b3808d75bb9..bfa8e6f3d84 100644 --- a/tests/rest/client/utils.py +++ b/tests/rest/client/utils.py @@ -48,6 +48,7 @@ from synapse.api.errors import Codes from synapse.server import HomeServer from synapse.types import JsonDict +from synapse.util.duration import Duration from tests.server import FakeChannel, make_request from tests.test_utils.html_parsers import TestHtmlParser @@ -453,6 +454,44 @@ def send_event( return channel.json_body + def send_sticky_event( + self, + room_id: str, + type: str, + *, + duration: Duration, + content: dict | None = None, + txn_id: str | None = None, + tok: str | None = None, + expect_code: int = HTTPStatus.OK, + custom_headers: Iterable[tuple[AnyStr, AnyStr]] | None = None, + ) -> JsonDict: + """ + Send an event that has a sticky duration according to MSC4354. + """ + + if txn_id is None: + txn_id = f"m{time.time()}" + + path = f"/_matrix/client/r0/rooms/{room_id}/send/{type}/{txn_id}?org.matrix.msc4354.sticky_duration_ms={duration.as_millis()}" + if tok: + path = path + f"&access_token={tok}" + + channel = make_request( + self.reactor, + self.site, + "PUT", + path, + content or {}, + custom_headers=custom_headers, + ) + + assert channel.code == expect_code, ( + f"Expected: {expect_code}, got: {channel.code}, resp: {channel.result['body']!r}" + ) + + return channel.json_body + def get_event( self, room_id: str, diff --git a/tests/storage/test_sticky_events.py b/tests/storage/test_sticky_events.py new file mode 100644 index 00000000000..60243cb2f40 --- /dev/null +++ b/tests/storage/test_sticky_events.py @@ -0,0 +1,278 @@ +# +# This file is licensed under the Affero General Public License (AGPL) version 3. +# +# Copyright (C) 2026 Element Creations Ltd. +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# See the GNU Affero General Public License for more details: +# . +import sqlite3 + +from twisted.internet.testing import MemoryReactor + +from synapse.api.constants import ( + EventContentFields, + EventTypes, + Membership, + StickyEvent, + StickyEventField, +) +from synapse.api.room_versions import RoomVersions +from synapse.rest import admin +from synapse.rest.client import login, register, room +from synapse.server import HomeServer +from synapse.types import JsonDict, create_requester +from synapse.util.clock import Clock +from synapse.util.duration import Duration + +from tests import unittest +from tests.utils import USE_POSTGRES_FOR_TESTS + + +class StickyEventsTestCase(unittest.HomeserverTestCase): + """ + Tests for the storage functions related to MSC4354: Sticky Events + """ + + if not USE_POSTGRES_FOR_TESTS and sqlite3.sqlite_version_info < (3, 40, 0): + # We need the JSON functionality in SQLite + skip = f"SQLite version is too old to support sticky events: {sqlite3.sqlite_version_info} (See https://github.com/element-hq/synapse/issues/19428)" + + servlets = [ + room.register_servlets, + login.register_servlets, + register.register_servlets, + admin.register_servlets, + ] + + def default_config(self) -> JsonDict: + config = super().default_config() + config["experimental_features"] = {"msc4354_enabled": True} + return config + + def prepare(self, reactor: MemoryReactor, clock: Clock, hs: HomeServer) -> None: + self.store = self.hs.get_datastores().main + + # Register an account and create a room + self.user_id = self.register_user("user", "pass") + self.token = self.login(self.user_id, "pass") + self.room_id = self.helper.create_room_as(self.user_id, tok=self.token) + + def test_get_updated_sticky_events(self) -> None: + """Test getting updated sticky events between stream IDs.""" + # Get the starting stream_id + start_id = self.store.get_max_sticky_events_stream_id() + + event_id_1 = self.helper.send_sticky_event( + self.room_id, + EventTypes.Message, + duration=Duration(minutes=1), + content={"body": "message 1", "msgtype": "m.text"}, + tok=self.token, + )["event_id"] + + mid_id = self.store.get_max_sticky_events_stream_id() + + event_id_2 = self.helper.send_sticky_event( + self.room_id, + EventTypes.Message, + duration=Duration(minutes=1), + content={"body": "message 2", "msgtype": "m.text"}, + tok=self.token, + )["event_id"] + + end_id = self.store.get_max_sticky_events_stream_id() + + # Get all updates + updates = self.get_success( + self.store.get_updated_sticky_events( + from_id=start_id, to_id=end_id, limit=10 + ) + ) + self.assertEqual(len(updates), 2) + self.assertEqual(updates[0].event_id, event_id_1) + self.assertEqual(updates[0].soft_failed, False) + self.assertEqual(updates[1].event_id, event_id_2) + self.assertEqual(updates[1].soft_failed, False) + + # Get only the second update + updates = self.get_success( + self.store.get_updated_sticky_events(from_id=mid_id, to_id=end_id, limit=10) + ) + self.assertEqual(len(updates), 1) + self.assertEqual(updates[0].event_id, event_id_2) + self.assertEqual(updates[0].soft_failed, False) + + def test_delete_expired_sticky_events(self) -> None: + """Test deletion of expired sticky events.""" + # Insert an expired event by advancing time past its duration + self.helper.send_sticky_event( + self.room_id, + EventTypes.Message, + duration=Duration(milliseconds=1), + content={"body": "expired message", "msgtype": "m.text"}, + tok=self.token, + ) + self.reactor.advance(0.002) + + # Insert a non-expired event + event_id_2 = self.helper.send_sticky_event( + self.room_id, + EventTypes.Message, + duration=Duration(minutes=1), + content={"body": "non-expired message", "msgtype": "m.text"}, + tok=self.token, + )["event_id"] + + end_id = self.store.get_max_sticky_events_stream_id() + + # Delete expired events + self.get_success(self.store._delete_expired_sticky_events()) + + # Check that only the non-expired event remains + sticky_events = self.get_success( + self.store.db_pool.simple_select_list( + table="sticky_events", keyvalues=None, retcols=("stream_id", "event_id") + ) + ) + self.assertEqual( + sticky_events, + [ + (end_id, event_id_2), + ], + ) + + def test_get_updated_sticky_events_with_limit(self) -> None: + """Test that the limit parameter works correctly.""" + # Get the starting stream_id + start_id = self.store.get_max_sticky_events_stream_id() + + event_id_1 = self.helper.send_sticky_event( + self.room_id, + EventTypes.Message, + duration=Duration(minutes=1), + content={"body": "message 1", "msgtype": "m.text"}, + tok=self.token, + )["event_id"] + + self.helper.send_sticky_event( + self.room_id, + EventTypes.Message, + duration=Duration(minutes=1), + content={"body": "message 2", "msgtype": "m.text"}, + tok=self.token, + ) + + # Get only the first update + updates = self.get_success( + self.store.get_updated_sticky_events( + from_id=start_id, to_id=start_id + 2, limit=1 + ) + ) + self.assertEqual(len(updates), 1) + self.assertEqual(updates[0].event_id, event_id_1) + + def test_outlier_events_not_in_table(self) -> None: + """ + Tests the behaviour of outliered and then de-outliered events in the + sticky_events table: they should only be added once they are de-outliered. + """ + persist_controller = self.hs.get_storage_controllers().persistence + assert persist_controller is not None + + user1_id = self.register_user("user1", "pass") + user2_id = self.register_user("user2", "pass") + user2_tok = self.login(user2_id, "pass") + + start_id = self.store.get_max_sticky_events_stream_id() + + room_id = self.helper.create_room_as( + user2_id, tok=user2_tok, room_version=RoomVersions.V10.identifier + ) + + # Create a membership event + event_dict = { + "type": EventTypes.Member, + "state_key": user1_id, + "sender": user1_id, + "room_id": room_id, + "content": {EventContentFields.MEMBERSHIP: Membership.JOIN}, + StickyEvent.EVENT_FIELD_NAME: StickyEventField( + duration_ms=Duration(hours=1).as_millis() + ), + } + + # Create the event twice: once as an outlier, once as a non-outlier. + # It's not at all obvious, but event creation before is deterministic + # (provided we don't change the forward extremities of the room!), + # so these two events are actually the same event with the same event ID. + ( + event_outlier, + unpersisted_context_outlier, + ) = self.get_success( + self.hs.get_event_creation_handler().create_event( + requester=create_requester(user1_id), + event_dict=event_dict, + outlier=True, + ) + ) + ( + event_non_outlier, + unpersisted_context_non_outlier, + ) = self.get_success( + self.hs.get_event_creation_handler().create_event( + requester=create_requester(user1_id), + event_dict=event_dict, + outlier=False, + ) + ) + + # Safety check that we're testing what we think we are + self.assertEqual(event_outlier.event_id, event_non_outlier.event_id) + + # Now persist the event as an outlier first of all + # FIXME: Should we use an `EventContext.for_outlier(...)` here? + # Doesn't seem to matter for this test. + context_outlier = self.get_success( + unpersisted_context_outlier.persist(event_outlier) + ) + self.get_success( + persist_controller.persist_event( + event_outlier, + context_outlier, + ) + ) + + # Since the event is outliered, it won't show up in the sticky_events table... + sticky_events = self.get_success( + self.store.db_pool.simple_select_list( + table="sticky_events", keyvalues=None, retcols=("stream_id", "event_id") + ) + ) + self.assertEqual(len(sticky_events), 0) + + # Now persist the event properly so that it gets de-outliered. + context_non_outlier = self.get_success( + unpersisted_context_non_outlier.persist(event_non_outlier) + ) + self.get_success( + persist_controller.persist_event( + event_non_outlier, + context_non_outlier, + ) + ) + + end_id = self.store.get_max_sticky_events_stream_id() + + # Check the event made it into the sticky_events table + updates = self.get_success( + self.store.get_updated_sticky_events( + from_id=start_id, to_id=end_id, limit=10 + ) + ) + self.assertEqual(len(updates), 1) + self.assertEqual(updates[0].event_id, event_non_outlier.event_id) From be362429deac94901c2c2b4b5d86ee68adad410a Mon Sep 17 00:00:00 2001 From: Quentin Gliech Date: Thu, 12 Feb 2026 16:39:59 +0100 Subject: [PATCH 013/121] Refuse requests and events signed by banned signing keys (#19459) Co-authored-by: Devon Hudson --- changelog.d/19459.misc | 1 + synapse/crypto/keyring.py | 23 ++++++++ tests/crypto/test_keyring.py | 47 +++++++++++++++- tests/federation/test_federation_base.py | 68 ++++++++++++++++++++++++ 4 files changed, 138 insertions(+), 1 deletion(-) create mode 100644 changelog.d/19459.misc create mode 100644 tests/federation/test_federation_base.py diff --git a/changelog.d/19459.misc b/changelog.d/19459.misc new file mode 100644 index 00000000000..9075af91ef6 --- /dev/null +++ b/changelog.d/19459.misc @@ -0,0 +1 @@ +Block federation requests and events authenticated using a known insecure signing key. See [CVE-2026-24044](https://www.cve.org/CVERecord?id=CVE-2026-24044) / [ELEMENTSEC-2025-1670](https://github.com/element-hq/ess-helm/security/advisories/GHSA-qwcj-h6m8-vp6q). diff --git a/synapse/crypto/keyring.py b/synapse/crypto/keyring.py index 883f682e776..0d4d5e0e172 100644 --- a/synapse/crypto/keyring.py +++ b/synapse/crypto/keyring.py @@ -22,6 +22,7 @@ import abc import logging from contextlib import ExitStack +from http import HTTPStatus from typing import TYPE_CHECKING, Callable, Iterable import attr @@ -60,6 +61,15 @@ logger = logging.getLogger(__name__) +# List of Unpadded Base64 server signing keys that are known to be vulnerable to attack. +# Incoming requests from homeservers using any of these keys should be refused. +# Events containing signatures using any of these keys should be refused. +BANNED_SERVER_SIGNING_KEYS = ( + # ELEMENTSEC-2025-1670 + "l/O9hxMVKB6Lg+3Hqf0FQQZhVESQcMzbPN1Cz2nM3og=", +) + + @attr.s(slots=True, frozen=True, cmp=False, auto_attribs=True) class VerifyJsonRequest: """ @@ -349,6 +359,19 @@ async def process_request(self, verify_request: VerifyJsonRequest) -> None: if key_result.valid_until_ts < verify_request.minimum_valid_until_ts: continue + key = encode_verify_key_base64(key_result.verify_key) + if key in BANNED_SERVER_SIGNING_KEYS: + raise SynapseError( + HTTPStatus.UNAUTHORIZED, + "Server signing key %s:%s for server %s has been banned by this server" + % ( + key_result.verify_key.alg, + key_result.verify_key.version, + verify_request.server_name, + ), + Codes.UNAUTHORIZED, + ) + await self.process_json(key_result.verify_key, verify_request) verified = True diff --git a/tests/crypto/test_keyring.py b/tests/crypto/test_keyring.py index 3cc905f699a..6bc935f2720 100644 --- a/tests/crypto/test_keyring.py +++ b/tests/crypto/test_keyring.py @@ -20,7 +20,7 @@ # import time from typing import Any, cast -from unittest.mock import Mock +from unittest.mock import Mock, patch import attr import canonicaljson @@ -238,6 +238,51 @@ def test_verify_json_for_server(self) -> None: # self.assertFalse(d.called) self.get_success(d) + def test_verify_json_for_server_using_banned_key(self) -> None: + """Ensure that JSON signed using a banned server_signing_key fails verification.""" + kr = keyring.Keyring(self.hs) + + banned_signing_key = signedjson.key.generate_signing_key("1") + r = self.hs.get_datastores().main.store_server_keys_response( + "server9", + from_server="test", + ts_added_ms=int(time.time() * 1000), + verify_keys={ + get_key_id(banned_signing_key): FetchKeyResult( + verify_key=get_verify_key(banned_signing_key), valid_until_ts=1000 + ) + }, + # The entire response gets signed & stored, just include the bits we + # care about. + response_json={ + "verify_keys": { + get_key_id(banned_signing_key): { + "key": encode_verify_key_base64( + get_verify_key(banned_signing_key) + ) + } + } + }, + ) + self.get_success(r) + + json1: JsonDict = {} + signedjson.sign.sign_json(json1, "server9", banned_signing_key) + + # Ensure the signatures check out normally + d = kr.verify_json_for_server("server9", json1, 500) + self.get_success(d) + + # Patch the list of banned signing keys and ensure the signature check fails + with patch.object( + keyring, + "BANNED_SERVER_SIGNING_KEYS", + (encode_verify_key_base64(get_verify_key(banned_signing_key))), + ): + # should fail on a signed object signed by the banned key + d = kr.verify_json_for_server("server9", json1, 500) + self.get_failure(d, SynapseError) + def test_verify_for_local_server(self) -> None: """Ensure that locally signed JSON can be verified without fetching keys over federation diff --git a/tests/federation/test_federation_base.py b/tests/federation/test_federation_base.py new file mode 100644 index 00000000000..1bc1da1feb7 --- /dev/null +++ b/tests/federation/test_federation_base.py @@ -0,0 +1,68 @@ +# +# This file is licensed under the Affero General Public License (AGPL) version 3. +# +# Copyright (C) 2026 New Vector, Ltd +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# See the GNU Affero General Public License for more details: +# . +# +# + + +from unittest.mock import patch + +from signedjson.key import encode_verify_key_base64, get_verify_key + +from synapse.crypto import keyring +from synapse.crypto.event_signing import add_hashes_and_signatures +from synapse.events import make_event_from_dict +from synapse.federation.federation_base import InvalidEventSignatureError + +from tests import unittest + + +class FederationBaseTestCase(unittest.HomeserverTestCase): + def test_events_signed_by_banned_key_are_refused(self) -> None: + """Ensure that event JSON signed using a banned server_signing_key fails verification.""" + event_dict = { + "content": {"body": "Here is the message content"}, + "event_id": "$0:domain", + "origin_server_ts": 1000000, + "type": "m.room.message", + "room_id": "!r:domain", + "sender": f"@u:{self.hs.config.server.server_name}", + "signatures": {}, + "unsigned": {"age_ts": 1000000}, + } + + add_hashes_and_signatures( + self.hs.config.server.default_room_version, + event_dict, + self.hs.config.server.server_name, + self.hs.signing_key, + ) + event = make_event_from_dict(event_dict) + fs = self.hs.get_federation_server() + + # Ensure the signatures check out normally + self.get_success( + fs._check_sigs_and_hash(self.hs.config.server.default_room_version, event) + ) + + # Patch the list of banned signing keys and ensure the signature check fails + with patch.object( + keyring, + "BANNED_SERVER_SIGNING_KEYS", + (encode_verify_key_base64(get_verify_key(self.hs.signing_key))), + ): + self.get_failure( + fs._check_sigs_and_hash( + self.hs.config.server.default_room_version, event + ), + InvalidEventSignatureError, + ) From 3fdeba35a6ed27e6d77bea9895863baef3d43c5d Mon Sep 17 00:00:00 2001 From: Quentin Gliech Date: Thu, 12 Feb 2026 16:46:02 +0100 Subject: [PATCH 014/121] 1.147.1 --- CHANGES.md | 9 +++++++++ changelog.d/19459.misc | 1 - debian/changelog | 6 ++++++ pyproject.toml | 2 +- 4 files changed, 16 insertions(+), 2 deletions(-) delete mode 100644 changelog.d/19459.misc diff --git a/CHANGES.md b/CHANGES.md index 2aac866df81..e7d27469dae 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,3 +1,12 @@ +# synapse 1.147.1 (2026-02-12) + +## Internal Changes + +- Block federation requests and events authenticated using a known insecure signing key. See [CVE-2026-24044](https://www.cve.org/CVERecord?id=CVE-2026-24044) / [ELEMENTSEC-2025-1670](https://github.com/element-hq/ess-helm/security/advisories/GHSA-qwcj-h6m8-vp6q). ([\#19459](https://github.com/element-hq/synapse/issues/19459)) + + + + # Synapse 1.147.0 (2026-02-10) No significant changes since 1.147.0rc1. diff --git a/changelog.d/19459.misc b/changelog.d/19459.misc deleted file mode 100644 index 9075af91ef6..00000000000 --- a/changelog.d/19459.misc +++ /dev/null @@ -1 +0,0 @@ -Block federation requests and events authenticated using a known insecure signing key. See [CVE-2026-24044](https://www.cve.org/CVERecord?id=CVE-2026-24044) / [ELEMENTSEC-2025-1670](https://github.com/element-hq/ess-helm/security/advisories/GHSA-qwcj-h6m8-vp6q). diff --git a/debian/changelog b/debian/changelog index 36a2921552f..a6852dac5e3 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,3 +1,9 @@ +matrix-synapse-py3 (1.147.1) stable; urgency=medium + + * New synapse release 1.147.1. + + -- Synapse Packaging team Thu, 12 Feb 2026 15:45:15 +0000 + matrix-synapse-py3 (1.147.0) stable; urgency=medium * New synapse release 1.147.0. diff --git a/pyproject.toml b/pyproject.toml index df10bdf19bd..8073f8ec44f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "matrix-synapse" -version = "1.147.0" +version = "1.147.1" description = "Homeserver for the Matrix decentralised comms protocol" readme = "README.rst" authors = [ From 7ff86876539581856ee2531de94a29fb61bbcc57 Mon Sep 17 00:00:00 2001 From: Quentin Gliech Date: Thu, 12 Feb 2026 16:50:45 +0100 Subject: [PATCH 015/121] Fuxup changelog --- CHANGES.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGES.md b/CHANGES.md index e7d27469dae..fe27ccb040a 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,4 +1,4 @@ -# synapse 1.147.1 (2026-02-12) +# Synapse 1.147.1 (2026-02-12) ## Internal Changes From a52ffd67e09bd2784ca7e2a2227a90c44c7e417c Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 17 Feb 2026 13:53:39 +0100 Subject: [PATCH 016/121] Bump cryptography from 46.0.3 to 46.0.5 (#19450) Bumps [cryptography](https://github.com/pyca/cryptography) from 46.0.3 to 46.0.5.
Changelog

Sourced from cryptography's changelog.

46.0.5 - 2026-02-10


* An attacker could create a malicious public key that reveals portions
of your
private key when using certain uncommon elliptic curves (binary curves).
This version now includes additional security checks to prevent this
attack.
This issue only affects binary elliptic curves, which are rarely used in
real-world applications. Credit to **XlabAI Team of Tencent Xuanwu Lab
and
Atuin Automated Vulnerability Discovery Engine** for reporting the
issue.
  **CVE-2026-26007**
* Support for ``SECT*`` binary elliptic curves is deprecated and will be
  removed in the next release.

.. v46-0-4:

46.0.4 - 2026-01-27

  • Dropped support for win_arm64 wheels_.
  • Updated Windows, macOS, and Linux wheels to be compiled with OpenSSL 3.5.5.

.. _v46-0-3:

Commits

[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=cryptography&package-manager=pip&previous-version=46.0.3&new-version=46.0.5)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores) Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`. [//]: # (dependabot-automerge-start) [//]: # (dependabot-automerge-end) ---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot show ignore conditions` will show all of the ignore conditions of the specified dependency - `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself) You can disable automated security fix PRs for this repo from the [Security Alerts page](https://github.com/element-hq/synapse/network/alerts).
Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- poetry.lock | 107 +++++++++++++++++++++++++--------------------------- 1 file changed, 51 insertions(+), 56 deletions(-) diff --git a/poetry.lock b/poetry.lock index 2d75fb50c85..5f1256676e9 100644 --- a/poetry.lock +++ b/poetry.lock @@ -398,66 +398,61 @@ files = [ [[package]] name = "cryptography" -version = "46.0.3" +version = "46.0.5" description = "cryptography is a package which provides cryptographic recipes and primitives to Python developers." optional = false python-versions = "!=3.9.0,!=3.9.1,>=3.8" groups = ["main", "dev"] files = [ - {file = "cryptography-46.0.3-cp311-abi3-macosx_10_9_universal2.whl", hash = "sha256:109d4ddfadf17e8e7779c39f9b18111a09efb969a301a31e987416a0191ed93a"}, - {file = "cryptography-46.0.3-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:09859af8466b69bc3c27bdf4f5d84a665e0f7ab5088412e9e2ec49758eca5cbc"}, - {file = "cryptography-46.0.3-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:01ca9ff2885f3acc98c29f1860552e37f6d7c7d013d7334ff2a9de43a449315d"}, - {file = "cryptography-46.0.3-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:6eae65d4c3d33da080cff9c4ab1f711b15c1d9760809dad6ea763f3812d254cb"}, - {file = "cryptography-46.0.3-cp311-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:e5bf0ed4490068a2e72ac03d786693adeb909981cc596425d09032d372bcc849"}, - {file = "cryptography-46.0.3-cp311-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:5ecfccd2329e37e9b7112a888e76d9feca2347f12f37918facbb893d7bb88ee8"}, - {file = "cryptography-46.0.3-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:a2c0cd47381a3229c403062f764160d57d4d175e022c1df84e168c6251a22eec"}, - {file = "cryptography-46.0.3-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:549e234ff32571b1f4076ac269fcce7a808d3bf98b76c8dd560e42dbc66d7d91"}, - {file = "cryptography-46.0.3-cp311-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:c0a7bb1a68a5d3471880e264621346c48665b3bf1c3759d682fc0864c540bd9e"}, - {file = "cryptography-46.0.3-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:10b01676fc208c3e6feeb25a8b83d81767e8059e1fe86e1dc62d10a3018fa926"}, - {file = "cryptography-46.0.3-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:0abf1ffd6e57c67e92af68330d05760b7b7efb243aab8377e583284dbab72c71"}, - {file = "cryptography-46.0.3-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:a04bee9ab6a4da801eb9b51f1b708a1b5b5c9eb48c03f74198464c66f0d344ac"}, - {file = "cryptography-46.0.3-cp311-abi3-win32.whl", hash = "sha256:f260d0d41e9b4da1ed1e0f1ce571f97fe370b152ab18778e9e8f67d6af432018"}, - {file = "cryptography-46.0.3-cp311-abi3-win_amd64.whl", hash = "sha256:a9a3008438615669153eb86b26b61e09993921ebdd75385ddd748702c5adfddb"}, - {file = "cryptography-46.0.3-cp311-abi3-win_arm64.whl", hash = "sha256:5d7f93296ee28f68447397bf5198428c9aeeab45705a55d53a6343455dcb2c3c"}, - {file = "cryptography-46.0.3-cp314-cp314t-macosx_10_9_universal2.whl", hash = "sha256:00a5e7e87938e5ff9ff5447ab086a5706a957137e6e433841e9d24f38a065217"}, - {file = "cryptography-46.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:c8daeb2d2174beb4575b77482320303f3d39b8e81153da4f0fb08eb5fe86a6c5"}, - {file = "cryptography-46.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:39b6755623145ad5eff1dab323f4eae2a32a77a7abef2c5089a04a3d04366715"}, - {file = "cryptography-46.0.3-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:db391fa7c66df6762ee3f00c95a89e6d428f4d60e7abc8328f4fe155b5ac6e54"}, - {file = "cryptography-46.0.3-cp314-cp314t-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:78a97cf6a8839a48c49271cdcbd5cf37ca2c1d6b7fdd86cc864f302b5e9bf459"}, - {file = "cryptography-46.0.3-cp314-cp314t-manylinux_2_28_ppc64le.whl", hash = "sha256:dfb781ff7eaa91a6f7fd41776ec37c5853c795d3b358d4896fdbb5df168af422"}, - {file = "cryptography-46.0.3-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:6f61efb26e76c45c4a227835ddeae96d83624fb0d29eb5df5b96e14ed1a0afb7"}, - {file = "cryptography-46.0.3-cp314-cp314t-manylinux_2_34_aarch64.whl", hash = "sha256:23b1a8f26e43f47ceb6d6a43115f33a5a37d57df4ea0ca295b780ae8546e8044"}, - {file = "cryptography-46.0.3-cp314-cp314t-manylinux_2_34_ppc64le.whl", hash = "sha256:b419ae593c86b87014b9be7396b385491ad7f320bde96826d0dd174459e54665"}, - {file = "cryptography-46.0.3-cp314-cp314t-manylinux_2_34_x86_64.whl", hash = "sha256:50fc3343ac490c6b08c0cf0d704e881d0d660be923fd3076db3e932007e726e3"}, - {file = "cryptography-46.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:22d7e97932f511d6b0b04f2bfd818d73dcd5928db509460aaf48384778eb6d20"}, - {file = "cryptography-46.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:d55f3dffadd674514ad19451161118fd010988540cee43d8bc20675e775925de"}, - {file = "cryptography-46.0.3-cp314-cp314t-win32.whl", hash = "sha256:8a6e050cb6164d3f830453754094c086ff2d0b2f3a897a1d9820f6139a1f0914"}, - {file = "cryptography-46.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:760f83faa07f8b64e9c33fc963d790a2edb24efb479e3520c14a45741cd9b2db"}, - {file = "cryptography-46.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:516ea134e703e9fe26bcd1277a4b59ad30586ea90c365a87781d7887a646fe21"}, - {file = "cryptography-46.0.3-cp38-abi3-macosx_10_9_universal2.whl", hash = "sha256:cb3d760a6117f621261d662bccc8ef5bc32ca673e037c83fbe565324f5c46936"}, - {file = "cryptography-46.0.3-cp38-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:4b7387121ac7d15e550f5cb4a43aef2559ed759c35df7336c402bb8275ac9683"}, - {file = "cryptography-46.0.3-cp38-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:15ab9b093e8f09daab0f2159bb7e47532596075139dd74365da52ecc9cb46c5d"}, - {file = "cryptography-46.0.3-cp38-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:46acf53b40ea38f9c6c229599a4a13f0d46a6c3fa9ef19fc1a124d62e338dfa0"}, - {file = "cryptography-46.0.3-cp38-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:10ca84c4668d066a9878890047f03546f3ae0a6b8b39b697457b7757aaf18dbc"}, - {file = "cryptography-46.0.3-cp38-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:36e627112085bb3b81b19fed209c05ce2a52ee8b15d161b7c643a7d5a88491f3"}, - {file = "cryptography-46.0.3-cp38-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:1000713389b75c449a6e979ffc7dcc8ac90b437048766cef052d4d30b8220971"}, - {file = "cryptography-46.0.3-cp38-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:b02cf04496f6576afffef5ddd04a0cb7d49cf6be16a9059d793a30b035f6b6ac"}, - {file = "cryptography-46.0.3-cp38-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:71e842ec9bc7abf543b47cf86b9a743baa95f4677d22baa4c7d5c69e49e9bc04"}, - {file = "cryptography-46.0.3-cp38-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:402b58fc32614f00980b66d6e56a5b4118e6cb362ae8f3fda141ba4689bd4506"}, - {file = "cryptography-46.0.3-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:ef639cb3372f69ec44915fafcd6698b6cc78fbe0c2ea41be867f6ed612811963"}, - {file = "cryptography-46.0.3-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:3b51b8ca4f1c6453d8829e1eb7299499ca7f313900dd4d89a24b8b87c0a780d4"}, - {file = "cryptography-46.0.3-cp38-abi3-win32.whl", hash = "sha256:6276eb85ef938dc035d59b87c8a7dc559a232f954962520137529d77b18ff1df"}, - {file = "cryptography-46.0.3-cp38-abi3-win_amd64.whl", hash = "sha256:416260257577718c05135c55958b674000baef9a1c7d9e8f306ec60d71db850f"}, - {file = "cryptography-46.0.3-cp38-abi3-win_arm64.whl", hash = "sha256:d89c3468de4cdc4f08a57e214384d0471911a3830fcdaf7a8cc587e42a866372"}, - {file = "cryptography-46.0.3-pp310-pypy310_pp73-macosx_10_9_x86_64.whl", hash = "sha256:a23582810fedb8c0bc47524558fb6c56aac3fc252cb306072fd2815da2a47c32"}, - {file = "cryptography-46.0.3-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:e7aec276d68421f9574040c26e2a7c3771060bc0cff408bae1dcb19d3ab1e63c"}, - {file = "cryptography-46.0.3-pp311-pypy311_pp73-macosx_10_9_x86_64.whl", hash = "sha256:7ce938a99998ed3c8aa7e7272dca1a610401ede816d36d0693907d863b10d9ea"}, - {file = "cryptography-46.0.3-pp311-pypy311_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:191bb60a7be5e6f54e30ba16fdfae78ad3a342a0599eb4193ba88e3f3d6e185b"}, - {file = "cryptography-46.0.3-pp311-pypy311_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:c70cc23f12726be8f8bc72e41d5065d77e4515efae3690326764ea1b07845cfb"}, - {file = "cryptography-46.0.3-pp311-pypy311_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:9394673a9f4de09e28b5356e7fff97d778f8abad85c9d5ac4a4b7e25a0de7717"}, - {file = "cryptography-46.0.3-pp311-pypy311_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:94cd0549accc38d1494e1f8de71eca837d0509d0d44bf11d158524b0e12cebf9"}, - {file = "cryptography-46.0.3-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:6b5063083824e5509fdba180721d55909ffacccc8adbec85268b48439423d78c"}, - {file = "cryptography-46.0.3.tar.gz", hash = "sha256:a8b17438104fed022ce745b362294d9ce35b4c2e45c1d958ad4a4b019285f4a1"}, + {file = "cryptography-46.0.5-cp311-abi3-macosx_10_9_universal2.whl", hash = "sha256:351695ada9ea9618b3500b490ad54c739860883df6c1f555e088eaf25b1bbaad"}, + {file = "cryptography-46.0.5-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:c18ff11e86df2e28854939acde2d003f7984f721eba450b56a200ad90eeb0e6b"}, + {file = "cryptography-46.0.5-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:4d7e3d356b8cd4ea5aff04f129d5f66ebdc7b6f8eae802b93739ed520c47c79b"}, + {file = "cryptography-46.0.5-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:50bfb6925eff619c9c023b967d5b77a54e04256c4281b0e21336a130cd7fc263"}, + {file = "cryptography-46.0.5-cp311-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:803812e111e75d1aa73690d2facc295eaefd4439be1023fefc4995eaea2af90d"}, + {file = "cryptography-46.0.5-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:3ee190460e2fbe447175cda91b88b84ae8322a104fc27766ad09428754a618ed"}, + {file = "cryptography-46.0.5-cp311-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:f145bba11b878005c496e93e257c1e88f154d278d2638e6450d17e0f31e558d2"}, + {file = "cryptography-46.0.5-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:e9251e3be159d1020c4030bd2e5f84d6a43fe54b6c19c12f51cde9542a2817b2"}, + {file = "cryptography-46.0.5-cp311-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:47fb8a66058b80e509c47118ef8a75d14c455e81ac369050f20ba0d23e77fee0"}, + {file = "cryptography-46.0.5-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:4c3341037c136030cb46e4b1e17b7418ea4cbd9dd207e4a6f3b2b24e0d4ac731"}, + {file = "cryptography-46.0.5-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:890bcb4abd5a2d3f852196437129eb3667d62630333aacc13dfd470fad3aaa82"}, + {file = "cryptography-46.0.5-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:80a8d7bfdf38f87ca30a5391c0c9ce4ed2926918e017c29ddf643d0ed2778ea1"}, + {file = "cryptography-46.0.5-cp311-abi3-win32.whl", hash = "sha256:60ee7e19e95104d4c03871d7d7dfb3d22ef8a9b9c6778c94e1c8fcc8365afd48"}, + {file = "cryptography-46.0.5-cp311-abi3-win_amd64.whl", hash = "sha256:38946c54b16c885c72c4f59846be9743d699eee2b69b6988e0a00a01f46a61a4"}, + {file = "cryptography-46.0.5-cp314-cp314t-macosx_10_9_universal2.whl", hash = "sha256:94a76daa32eb78d61339aff7952ea819b1734b46f73646a07decb40e5b3448e2"}, + {file = "cryptography-46.0.5-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:5be7bf2fb40769e05739dd0046e7b26f9d4670badc7b032d6ce4db64dddc0678"}, + {file = "cryptography-46.0.5-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:fe346b143ff9685e40192a4960938545c699054ba11d4f9029f94751e3f71d87"}, + {file = "cryptography-46.0.5-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:c69fd885df7d089548a42d5ec05be26050ebcd2283d89b3d30676eb32ff87dee"}, + {file = "cryptography-46.0.5-cp314-cp314t-manylinux_2_28_ppc64le.whl", hash = "sha256:8293f3dea7fc929ef7240796ba231413afa7b68ce38fd21da2995549f5961981"}, + {file = "cryptography-46.0.5-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:1abfdb89b41c3be0365328a410baa9df3ff8a9110fb75e7b52e66803ddabc9a9"}, + {file = "cryptography-46.0.5-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:d66e421495fdb797610a08f43b05269e0a5ea7f5e652a89bfd5a7d3c1dee3648"}, + {file = "cryptography-46.0.5-cp314-cp314t-manylinux_2_34_aarch64.whl", hash = "sha256:4e817a8920bfbcff8940ecfd60f23d01836408242b30f1a708d93198393a80b4"}, + {file = "cryptography-46.0.5-cp314-cp314t-manylinux_2_34_ppc64le.whl", hash = "sha256:68f68d13f2e1cb95163fa3b4db4bf9a159a418f5f6e7242564fc75fcae667fd0"}, + {file = "cryptography-46.0.5-cp314-cp314t-manylinux_2_34_x86_64.whl", hash = "sha256:a3d1fae9863299076f05cb8a778c467578262fae09f9dc0ee9b12eb4268ce663"}, + {file = "cryptography-46.0.5-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:c4143987a42a2397f2fc3b4d7e3a7d313fbe684f67ff443999e803dd75a76826"}, + {file = "cryptography-46.0.5-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:7d731d4b107030987fd61a7f8ab512b25b53cef8f233a97379ede116f30eb67d"}, + {file = "cryptography-46.0.5-cp314-cp314t-win32.whl", hash = "sha256:c3bcce8521d785d510b2aad26ae2c966092b7daa8f45dd8f44734a104dc0bc1a"}, + {file = "cryptography-46.0.5-cp314-cp314t-win_amd64.whl", hash = "sha256:4d8ae8659ab18c65ced284993c2265910f6c9e650189d4e3f68445ef82a810e4"}, + {file = "cryptography-46.0.5-cp38-abi3-macosx_10_9_universal2.whl", hash = "sha256:4108d4c09fbbf2789d0c926eb4152ae1760d5a2d97612b92d508d96c861e4d31"}, + {file = "cryptography-46.0.5-cp38-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:7d1f30a86d2757199cb2d56e48cce14deddf1f9c95f1ef1b64ee91ea43fe2e18"}, + {file = "cryptography-46.0.5-cp38-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:039917b0dc418bb9f6edce8a906572d69e74bd330b0b3fea4f79dab7f8ddd235"}, + {file = "cryptography-46.0.5-cp38-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:ba2a27ff02f48193fc4daeadf8ad2590516fa3d0adeeb34336b96f7fa64c1e3a"}, + {file = "cryptography-46.0.5-cp38-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:61aa400dce22cb001a98014f647dc21cda08f7915ceb95df0c9eaf84b4b6af76"}, + {file = "cryptography-46.0.5-cp38-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:3ce58ba46e1bc2aac4f7d9290223cead56743fa6ab94a5d53292ffaac6a91614"}, + {file = "cryptography-46.0.5-cp38-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:420d0e909050490d04359e7fdb5ed7e667ca5c3c402b809ae2563d7e66a92229"}, + {file = "cryptography-46.0.5-cp38-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:582f5fcd2afa31622f317f80426a027f30dc792e9c80ffee87b993200ea115f1"}, + {file = "cryptography-46.0.5-cp38-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:bfd56bb4b37ed4f330b82402f6f435845a5f5648edf1ad497da51a8452d5d62d"}, + {file = "cryptography-46.0.5-cp38-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:a3d507bb6a513ca96ba84443226af944b0f7f47dcc9a399d110cd6146481d24c"}, + {file = "cryptography-46.0.5-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:9f16fbdf4da055efb21c22d81b89f155f02ba420558db21288b3d0035bafd5f4"}, + {file = "cryptography-46.0.5-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:ced80795227d70549a411a4ab66e8ce307899fad2220ce5ab2f296e687eacde9"}, + {file = "cryptography-46.0.5-cp38-abi3-win32.whl", hash = "sha256:02f547fce831f5096c9a567fd41bc12ca8f11df260959ecc7c3202555cc47a72"}, + {file = "cryptography-46.0.5-cp38-abi3-win_amd64.whl", hash = "sha256:556e106ee01aa13484ce9b0239bca667be5004efb0aabbed28d353df86445595"}, + {file = "cryptography-46.0.5-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:3b4995dc971c9fb83c25aa44cf45f02ba86f71ee600d81091c2f0cbae116b06c"}, + {file = "cryptography-46.0.5-pp311-pypy311_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:bc84e875994c3b445871ea7181d424588171efec3e185dced958dad9e001950a"}, + {file = "cryptography-46.0.5-pp311-pypy311_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:2ae6971afd6246710480e3f15824ed3029a60fc16991db250034efd0b9fb4356"}, + {file = "cryptography-46.0.5-pp311-pypy311_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:d861ee9e76ace6cf36a6a89b959ec08e7bc2493ee39d07ffe5acb23ef46d27da"}, + {file = "cryptography-46.0.5-pp311-pypy311_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:2b7a67c9cd56372f3249b39699f2ad479f6991e62ea15800973b956f4b73e257"}, + {file = "cryptography-46.0.5-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:8456928655f856c6e1533ff59d5be76578a7157224dbd9ce6872f25055ab9ab7"}, + {file = "cryptography-46.0.5.tar.gz", hash = "sha256:abace499247268e3757271b2f1e244b36b06f8515cf27c4d49468fc9eb16e93d"}, ] [package.dependencies] @@ -471,7 +466,7 @@ nox = ["nox[uv] (>=2024.4.15)"] pep8test = ["check-sdist", "click (>=8.0.1)", "mypy (>=1.14)", "ruff (>=0.11.11)"] sdist = ["build (>=1.0.0)"] ssh = ["bcrypt (>=3.1.5)"] -test = ["certifi (>=2024)", "cryptography-vectors (==46.0.3)", "pretend (>=0.7)", "pytest (>=7.4.0)", "pytest-benchmark (>=4.0)", "pytest-cov (>=2.10.1)", "pytest-xdist (>=3.5.0)"] +test = ["certifi (>=2024)", "cryptography-vectors (==46.0.5)", "pretend (>=0.7)", "pytest (>=7.4.0)", "pytest-benchmark (>=4.0)", "pytest-cov (>=2.10.1)", "pytest-xdist (>=3.5.0)"] test-randomorder = ["pytest-randomly"] [[package]] From 27b2915606dfb0fc55ccb063227747eb0816f641 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 17 Feb 2026 13:55:06 +0100 Subject: [PATCH 017/121] Bump tokio from 1.48.0 to 1.49.0 (#19443) Bumps [tokio](https://github.com/tokio-rs/tokio) from 1.48.0 to 1.49.0.
Release notes

Sourced from tokio's releases.

Tokio v1.49.0

1.49.0 (January 3rd, 2026)

Added

  • net: add support for TCLASS option on IPv6 (#7781)
  • runtime: stabilize runtime::id::Id (#7125)
  • task: implement Extend for JoinSet (#7195)
  • task: stabilize the LocalSet::id() (#7776)

Changed

  • net: deprecate {TcpStream,TcpSocket}::set_linger (#7752)

Fixed

  • macros: fix the hygiene issue of join! and try_join! (#7766)
  • runtime: revert "replace manual vtable definitions with Wake" (#7699)
  • sync: return TryRecvError::Disconnected from Receiver::try_recv after Receiver::close (#7686)
  • task: remove unnecessary trait bounds on the Debug implementation (#7720)

Unstable

  • fs: handle EINTR in fs::write for io-uring (#7786)
  • fs: support io-uring with tokio::fs::read (#7696)
  • runtime: disable io-uring on EPERM (#7724)
  • time: add alternative timer for better multicore scalability (#7467)

Documented

  • docs: fix a typos in bounded.rs and park.rs (#7817)
  • io: add SyncIoBridge cross-references to copy and copy_buf (#7798)
  • io: doc that AsyncWrite does not inherit from std::io::Write (#7705)
  • metrics: clarify that num_alive_tasks is not strongly consistent (#7614)
  • net: clarify the cancellation safety of the TcpStream::peek (#7305)
  • net: clarify the drop behavior of unix::OwnedWriteHalf (#7742)
  • net: clarify the platform-dependent backlog in TcpSocket docs (#7738)
  • runtime: mention LocalRuntime in new_current_thread docs (#7820)
  • sync: add missing period to mpsc::Sender::try_send docs (#7721)
  • sync: clarify the cancellation safety of oneshot::Receiver (#7780)
  • sync: improve the docs for the errors of mpsc (#7722)
  • task: add example for spawn_local usage on local runtime (#7689)

#7125: tokio-rs/tokio#7125 #7195: tokio-rs/tokio#7195 #7305: tokio-rs/tokio#7305 #7467: tokio-rs/tokio#7467 #7614: tokio-rs/tokio#7614 #7686: tokio-rs/tokio#7686 #7689: tokio-rs/tokio#7689

... (truncated)

Commits

[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=tokio&package-manager=cargo&previous-version=1.48.0&new-version=1.49.0)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores) Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`. [//]: # (dependabot-automerge-start) [//]: # (dependabot-automerge-end) ---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot show ignore conditions` will show all of the ignore conditions of the specified dependency - `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself)
Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- Cargo.lock | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index e1344f89d61..c981d139d4a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1416,9 +1416,9 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "tokio" -version = "1.48.0" +version = "1.49.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ff360e02eab121e0bc37a2d3b4d4dc622e6eda3a8e5253d5435ecf5bd4c68408" +checksum = "72a2903cd7736441aac9df9d7688bd0ce48edccaadf181c3b90be801e81d3d86" dependencies = [ "bytes", "libc", From 5be475f5a24b01df8e652838967b2809b78c6a43 Mon Sep 17 00:00:00 2001 From: Quentin Gliech Date: Tue, 17 Feb 2026 13:57:14 +0100 Subject: [PATCH 018/121] Allow configuring the Rust HTTP client to use HTTP/2 only (#19457) This allows the Rust HTTP client to be configured to force HTTP/2 even on plaintext connections. This is useful in contexts where the remote server is known to server HTTP/2 over plain text. Added because we use the Synapse Rust HTTP client with the Synapse Pro `event-cache` module. We use this because it's independent from the Python reactor which makes things slower than expected. Currently, the Synapse Rust HTTP client uses HTTP/1 which means a new connection for every request. With HTTP/2, we can share the connection across requests. We want to see if this will make a performance difference and less stress on the database connection situation, see https://github.com/element-hq/synapse-rust-apps/issues/452#issuecomment-3897717599 Here is the sibling PR for using HTTP/2 on the Synapse Pro `event-cache` module side: https://github.com/element-hq/synapse-pro-modules/pull/35 --- changelog.d/19457.misc | 1 + rust/src/http_client.rs | 22 +++++++++++++++++----- synapse/synapse_rust/http_client.pyi | 20 +++++++++++++++++++- 3 files changed, 37 insertions(+), 6 deletions(-) create mode 100644 changelog.d/19457.misc diff --git a/changelog.d/19457.misc b/changelog.d/19457.misc new file mode 100644 index 00000000000..ccbb7f5b144 --- /dev/null +++ b/changelog.d/19457.misc @@ -0,0 +1 @@ +Allow configuring the Rust HTTP client to use HTTP/2 only. diff --git a/rust/src/http_client.rs b/rust/src/http_client.rs index b1e4f753b82..9bbdff8b454 100644 --- a/rust/src/http_client.rs +++ b/rust/src/http_client.rs @@ -171,15 +171,27 @@ struct HttpClient { #[pymethods] impl HttpClient { #[new] - pub fn py_new(reactor: Bound, user_agent: &str) -> PyResult { + #[pyo3(signature = (reactor, user_agent, http2_only = false))] + pub fn py_new( + reactor: Bound, + user_agent: &str, + http2_only: bool, + ) -> PyResult { // Make sure the runtime gets installed let _ = runtime(&reactor)?; + let mut builder = reqwest::Client::builder().user_agent(user_agent); + + if http2_only { + // Create the client with 'HTTP/2 prior knowledge' enabled, which + // means it will always use HTTP/2 for unencrypted connections + builder = builder.http2_prior_knowledge(); + } + + let client = builder.build().context("building reqwest client")?; + Ok(HttpClient { - client: reqwest::Client::builder() - .user_agent(user_agent) - .build() - .context("building reqwest client")?, + client, reactor: reactor.unbind(), }) } diff --git a/synapse/synapse_rust/http_client.pyi b/synapse/synapse_rust/http_client.pyi index 530d2be8e38..1814daec73e 100644 --- a/synapse/synapse_rust/http_client.pyi +++ b/synapse/synapse_rust/http_client.pyi @@ -21,7 +21,25 @@ class HttpClient: The returned deferreds follow Synapse logcontext rules. """ - def __init__(self, reactor: ISynapseReactor, user_agent: str) -> None: ... + def __init__( + self, + reactor: ISynapseReactor, + user_agent: str, + http2_only: bool = False, + ) -> None: + """ + Create a new HTTP client backed by reqwest. + + Args: + reactor: The Twisted reactor to coordinate with + user_agent: The user agent to use for requests + http2_only: Whether to use HTTP/2 only, even on unencrypted connections. By + default, it will always use HTTP/1.1 over unencrypted connections, and + rely on TLS ALPN to negotiate HTTP/2. + + Ensure the upstream server supports HTTP/2 before enabling this. + """ + def get(self, url: str, response_limit: int) -> Deferred[bytes]: ... def post( self, From 1592afba8d6d8421febe93162565edede48c9992 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 17 Feb 2026 13:58:10 +0100 Subject: [PATCH 019/121] Bump github.com/docker/docker from 28.2.2+incompatible to 28.3.3+incompatible in /complement (#19436) Bumps [github.com/docker/docker](https://github.com/docker/docker) from 28.2.2+incompatible to 28.3.3+incompatible.
Release notes

Sourced from github.com/docker/docker's releases.

v28.3.3

28.3.3

For a full list of pull requests and changes in this release, refer to the relevant GitHub milestones:

Security

This release fixes an issue where, after a firewalld reload, published container ports could be accessed directly from the local network, even when they were intended to be accessible only via a loopback address. CVE-2025-54388 / GHSA-x4rx-4gw3-53p4 / moby/moby#50506.

Packaging updates

Go SDK

  • cli/command/formatter: add TrunateID() utility as alternative for github.com/docker/docker/pkg/stringid.TrunateID(). docker/cli#6180

28.3.2

For a full list of pull requests and changes in this release, refer to the relevant GitHub milestones:

Bug fixes and enhancements

  • Fix --use-api-socket not working correctly when targeting a remote daemon. docker/cli#6157
  • Fix stray "otel error" logs being printed if debug logging is enabled. docker/cli#6160
  • Quote SSH arguments when connecting to a remote daemon over an SSH connection to avoid unexpected expansion. docker/cli#6147
  • Warn when DOCKER_AUTH_CONFIG is set during docker login and docker logout. docker/cli#6163

Packaging updates

28.3.1

For a full list of pull requests and changes in this release, refer to the relevant GitHub milestones:

... (truncated)

Commits
  • bea959c Merge pull request #50506 from robmry/backport-28.x/fix_firewalld_reload
  • 3e9ff78 bridge: Reapply endpoint iptables rules on firewalld reload
  • 29ed80a bridge: Trigger firewalld reload during bridge integration tests
  • da489a1 Merge pull request #50478 from thaJeztah/28.x_backport_gha_bump_bk
  • f173e45 Merge pull request #50480 from austinvazquez/cherry-pick-ea29dffaa541289591aa...
  • e4b1f89 daemon/server: remove compatibility with API v1.4 auth-config on push
  • 0c9e14d hack/buildkit-ref: temporarily bump BuildKit to head of v0.23 branch
  • bf6d688 Merge pull request #50471 from austinvazquez/cherry-pick-b1ce0c89f0214cc6711c...
  • 4205776 client: always send (empty) body on push
  • e77ff99 Merge pull request #50354 from vvoland/50353-28.x
  • Additional commits viewable in compare view

[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=github.com/docker/docker&package-manager=go_modules&previous-version=28.2.2+incompatible&new-version=28.3.3+incompatible)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores) Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`. [//]: # (dependabot-automerge-start) [//]: # (dependabot-automerge-end) ---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot show ignore conditions` will show all of the ignore conditions of the specified dependency - `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself) You can disable automated security fix PRs for this repo from the [Security Alerts page](https://github.com/element-hq/synapse/network/alerts).
Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- complement/go.mod | 158 +---------------- complement/go.sum | 422 +--------------------------------------------- 2 files changed, 5 insertions(+), 575 deletions(-) diff --git a/complement/go.mod b/complement/go.mod index 062ea8e6ee9..c6c1678bacc 100644 --- a/complement/go.mod +++ b/complement/go.mod @@ -5,171 +5,44 @@ go 1.24.1 toolchain go1.24.4 require ( - github.com/docker/docker v28.2.2+incompatible - github.com/docker/go-connections v0.5.0 github.com/matrix-org/complement v0.0.0-20251120181401-44111a2a8a9d github.com/matrix-org/gomatrixserverlib v0.0.0-20250813150445-9f5070a65744 - github.com/minio/operator v0.0.0-20250423184541-6eee6a7caa70 - github.com/mittwald/go-helm-client v0.12.18 - gopkg.in/yaml.v2 v2.4.0 - k8s.io/api v0.33.2 - k8s.io/apimachinery v0.33.2 - k8s.io/client-go v0.33.2 - sigs.k8s.io/controller-runtime v0.21.0 ) require ( - github.com/cloudnative-pg/barman-cloud v0.3.1 // indirect - github.com/cloudnative-pg/machinery v0.2.0 // indirect - github.com/dustin/go-humanize v1.0.1 // indirect - github.com/evanphx/json-patch/v5 v5.9.11 // indirect - github.com/fsnotify/fsnotify v1.8.0 // indirect - github.com/go-ini/ini v1.67.0 // indirect - github.com/go-logr/zapr v1.3.0 // indirect - github.com/go-ole/go-ole v1.3.0 // indirect - github.com/goccy/go-json v0.10.5 // indirect - github.com/golang-jwt/jwt/v4 v4.5.2 // indirect - github.com/golang/protobuf v1.5.4 // indirect + github.com/docker/docker v28.3.3+incompatible // indirect + github.com/docker/go-connections v0.5.0 // indirect github.com/hashicorp/go-set/v3 v3.0.0 // indirect - github.com/klauspost/cpuid/v2 v2.2.10 // indirect - github.com/kubernetes-csi/external-snapshotter/client/v8 v8.2.0 // indirect - github.com/lufia/plan9stats v0.0.0-20250317134145-8bc96cf8fc35 // indirect - github.com/matrix-org/gomatrix v0.0.0-20220926102614-ceba4d9f7530 // indirect - github.com/matttproud/golang_protobuf_extensions v1.0.4 // indirect - github.com/miekg/dns v1.1.66 // indirect - github.com/minio/crc64nvme v1.0.1 // indirect - github.com/minio/madmin-go/v3 v3.0.100 // indirect - github.com/minio/md5-simd v1.1.2 // indirect - github.com/minio/minio-go/v7 v7.0.89 // indirect github.com/moby/sys/atomicwriter v0.1.0 // indirect github.com/oleiade/lane/v2 v2.0.0 // indirect - github.com/philhofer/fwd v1.1.3-0.20240916144458-20a13a1f6b7c // indirect - github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 // indirect - github.com/prometheus-operator/prometheus-operator/pkg/apis/monitoring v0.80.1 // indirect - github.com/prometheus/prom2json v1.4.1 // indirect - github.com/prometheus/prometheus v0.302.1 // indirect - github.com/rs/xid v1.6.0 // indirect - github.com/safchain/ethtool v0.5.10 // indirect - github.com/secure-io/sio-go v0.3.1 // indirect - github.com/shirou/gopsutil/v3 v3.24.5 // indirect - github.com/shoenig/go-m1cpu v0.1.6 // indirect - github.com/tinylib/msgp v1.2.5 // indirect - github.com/tklauser/go-sysconf v0.3.15 // indirect - github.com/tklauser/numcpus v0.10.0 // indirect - github.com/yusufpapurcu/wmi v1.2.4 // indirect - go.uber.org/multierr v1.11.0 // indirect - go.uber.org/zap v1.27.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.34.0 // indirect golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 // indirect - golang.org/x/mod v0.29.0 // indirect - golang.org/x/tools v0.38.0 // indirect - gomodules.xyz/jsonpatch/v2 v2.4.0 // indirect gotest.tools/v3 v3.4.0 // indirect - oras.land/oras-go/v2 v2.6.0 // indirect ) require ( - dario.cat/mergo v1.0.1 // indirect - github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c // indirect - github.com/BurntSushi/toml v1.5.0 // indirect - github.com/MakeNowJust/heredoc v1.0.0 // indirect - github.com/Masterminds/goutils v1.1.1 // indirect - github.com/Masterminds/semver/v3 v3.3.1 // indirect - github.com/Masterminds/sprig/v3 v3.3.0 // indirect - github.com/Masterminds/squirrel v1.5.4 // indirect github.com/Microsoft/go-winio v0.6.2 // indirect - github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 // indirect - github.com/beorn7/perks v1.0.1 // indirect - github.com/blang/semver/v4 v4.0.0 // indirect - github.com/cespare/xxhash/v2 v2.3.0 // indirect - github.com/chai2010/gettext-go v1.0.2 // indirect - github.com/cloudnative-pg/cloudnative-pg v1.26.0 - github.com/containerd/containerd v1.7.27 // indirect github.com/containerd/errdefs v1.0.0 // indirect github.com/containerd/errdefs/pkg v0.3.0 // indirect - github.com/containerd/log v0.1.0 // indirect - github.com/containerd/platforms v0.2.1 // indirect - github.com/cyphar/filepath-securejoin v0.4.1 // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect github.com/distribution/reference v0.6.0 // indirect github.com/docker/go-units v0.5.0 // indirect - github.com/emicklei/go-restful/v3 v3.12.2 // indirect - github.com/evanphx/json-patch v5.9.11+incompatible // indirect - github.com/exponent-io/jsonpath v0.0.0-20210407135951-1de76d718b3f // indirect - github.com/fatih/color v1.18.0 // indirect github.com/felixge/httpsnoop v1.0.4 // indirect - github.com/fxamacker/cbor/v2 v2.7.0 // indirect - github.com/go-errors/errors v1.5.1 // indirect - github.com/go-gorp/gorp/v3 v3.1.0 // indirect github.com/go-logr/logr v1.4.3 // indirect github.com/go-logr/stdr v1.2.2 // indirect - github.com/go-openapi/jsonpointer v0.21.1 // indirect - github.com/go-openapi/jsonreference v0.21.0 // indirect - github.com/go-openapi/swag v0.23.1 // indirect - github.com/gobwas/glob v0.2.3 // indirect github.com/gogo/protobuf v1.3.2 // indirect - github.com/google/btree v1.1.3 // indirect - github.com/google/gnostic-models v0.6.9 // indirect - github.com/google/go-cmp v0.7.0 // indirect - github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 // indirect - github.com/google/uuid v1.6.0 // indirect - github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674 // indirect - github.com/gosuri/uitable v0.0.4 // indirect - github.com/gregjones/httpcache v0.0.0-20190611155906-901d90724c79 // indirect - github.com/hashicorp/errwrap v1.1.0 // indirect - github.com/hashicorp/go-multierror v1.1.1 // indirect - github.com/huandu/xstrings v1.5.0 // indirect - github.com/inconshreveable/mousetrap v1.1.0 // indirect - github.com/jmoiron/sqlx v1.4.0 // indirect - github.com/josharian/intern v1.0.0 // indirect - github.com/json-iterator/go v1.1.12 // indirect - github.com/klauspost/compress v1.18.0 // indirect - github.com/lann/builder v0.0.0-20180802200727-47ae307949d0 // indirect - github.com/lann/ps v0.0.0-20150810152359-62de8c46ede0 // indirect - github.com/lib/pq v1.10.9 // indirect - github.com/liggitt/tabwriter v0.0.0-20181228230101-89fcab3d43de // indirect - github.com/mailru/easyjson v0.9.0 // indirect github.com/matrix-org/util v0.0.0-20221111132719-399730281e66 // indirect - github.com/mattn/go-colorable v0.1.14 // indirect - github.com/mattn/go-isatty v0.0.20 // indirect - github.com/mattn/go-runewidth v0.0.15 // indirect - github.com/mitchellh/copystructure v1.2.0 // indirect - github.com/mitchellh/go-wordwrap v1.0.1 // indirect - github.com/mitchellh/reflectwalk v1.0.2 // indirect github.com/moby/docker-image-spec v1.3.1 // indirect - github.com/moby/spdystream v0.5.0 // indirect github.com/moby/term v0.5.2 // indirect - github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect - github.com/modern-go/reflect2 v1.0.2 // indirect - github.com/monochromegane/go-gitignore v0.0.0-20200626010858-205db1a8cc00 // indirect - github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect - github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f // indirect github.com/opencontainers/go-digest v1.0.0 // indirect github.com/opencontainers/image-spec v1.1.1 // indirect - github.com/peterbourgon/diskv v2.0.1+incompatible // indirect github.com/pkg/errors v0.9.1 // indirect github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect - github.com/prometheus/client_golang v1.22.0 // indirect - github.com/prometheus/client_model v0.6.2 - github.com/prometheus/common v0.65.0 - github.com/prometheus/procfs v0.16.0 // indirect - github.com/rivo/uniseg v0.4.4 // indirect - github.com/rubenv/sql-migrate v1.8.0 // indirect - github.com/russross/blackfriday/v2 v2.1.0 // indirect - github.com/shopspring/decimal v1.4.0 // indirect github.com/sirupsen/logrus v1.9.3 // indirect - github.com/spf13/cast v1.7.0 // indirect - github.com/spf13/cobra v1.9.1 // indirect - github.com/spf13/pflag v1.0.6 // indirect - github.com/stretchr/testify v1.10.0 github.com/tidwall/gjson v1.18.0 // indirect github.com/tidwall/match v1.1.1 // indirect github.com/tidwall/pretty v1.2.1 // indirect github.com/tidwall/sjson v1.2.5 // indirect - github.com/x448/float16 v0.8.4 // indirect - github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb // indirect - github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect - github.com/xeipuuv/gojsonschema v1.2.0 // indirect - github.com/xlab/treeprint v1.2.0 // indirect go.opentelemetry.io/auto/sdk v1.1.0 // indirect go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0 // indirect go.opentelemetry.io/otel v1.36.0 // indirect @@ -178,32 +51,7 @@ require ( go.opentelemetry.io/otel/trace v1.36.0 // indirect go.opentelemetry.io/proto/otlp v1.7.0 // indirect golang.org/x/crypto v0.45.0 // indirect - golang.org/x/net v0.47.0 // indirect - golang.org/x/oauth2 v0.30.0 // indirect - golang.org/x/sync v0.18.0 // indirect golang.org/x/sys v0.38.0 // indirect - golang.org/x/term v0.37.0 // indirect - golang.org/x/text v0.31.0 // indirect golang.org/x/time v0.11.0 // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20250528174236-200df99c418a // indirect - google.golang.org/grpc v1.72.2 // indirect google.golang.org/protobuf v1.36.6 // indirect - gopkg.in/evanphx/json-patch.v4 v4.12.0 // indirect - gopkg.in/inf.v0 v0.9.1 // indirect - gopkg.in/yaml.v3 v3.0.1 // indirect - helm.sh/helm/v3 v3.18.4 // indirect - k8s.io/apiextensions-apiserver v0.33.2 // indirect - k8s.io/apiserver v0.33.2 // indirect - k8s.io/cli-runtime v0.33.2 // indirect - k8s.io/component-base v0.33.2 // indirect - k8s.io/klog/v2 v2.130.1 - k8s.io/kube-openapi v0.0.0-20250318190949-c8a335a9a2ff // indirect - k8s.io/kubectl v0.33.2 // indirect - k8s.io/utils v0.0.0-20250502105355-0f33e8f1c979 // indirect - sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8 // indirect - sigs.k8s.io/kustomize/api v0.19.0 // indirect - sigs.k8s.io/kustomize/kyaml v0.19.0 // indirect - sigs.k8s.io/randfill v1.0.0 // indirect - sigs.k8s.io/structured-merge-diff/v4 v4.6.0 // indirect - sigs.k8s.io/yaml v1.4.0 // indirect ) diff --git a/complement/go.sum b/complement/go.sum index 2ff86089531..c674730c057 100644 --- a/complement/go.sum +++ b/complement/go.sum @@ -1,228 +1,47 @@ -dario.cat/mergo v1.0.1 h1:Ra4+bf83h2ztPIQYNP99R6m+Y7KfnARDfID+a+vLl4s= -dario.cat/mergo v1.0.1/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk= -filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA= -filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4= -github.com/AdaLogics/go-fuzz-headers v0.0.0-20230811130428-ced1acdcaa24 h1:bvDV9vkmnHYOMsOr4WLk+Vo07yKIzd94sVoIqshQ4bU= -github.com/AdaLogics/go-fuzz-headers v0.0.0-20230811130428-ced1acdcaa24/go.mod h1:8o94RPi1/7XTJvwPpRSzSUedZrtlirdB3r9Z20bi2f8= github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c h1:udKWzYgxTojEKWjV8V+WSxDXJ4NFATAsZjh8iIbsQIg= github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E= -github.com/BurntSushi/toml v1.5.0 h1:W5quZX/G/csjUnuI8SUYlsHs9M38FC7znL0lIO+DvMg= -github.com/BurntSushi/toml v1.5.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho= -github.com/DATA-DOG/go-sqlmock v1.5.2 h1:OcvFkGmslmlZibjAjaHm3L//6LiuBgolP7OputlJIzU= -github.com/DATA-DOG/go-sqlmock v1.5.2/go.mod h1:88MAG/4G7SMwSE3CeA0ZKzrT5CiOU3OJ+JlNzwDqpNU= -github.com/MakeNowJust/heredoc v1.0.0 h1:cXCdzVdstXyiTqTvfqk9SDHpKNjxuom+DOlyEeQ4pzQ= -github.com/MakeNowJust/heredoc v1.0.0/go.mod h1:mG5amYoWBHf8vpLOuehzbGGw0EHxpZZ6lCpQ4fNJ8LE= -github.com/Masterminds/goutils v1.1.1 h1:5nUrii3FMTL5diU80unEVvNevw1nH4+ZV4DSLVJLSYI= -github.com/Masterminds/goutils v1.1.1/go.mod h1:8cTjp+g8YejhMuvIA5y2vz3BpJxksy863GQaJW2MFNU= -github.com/Masterminds/semver/v3 v3.3.1 h1:QtNSWtVZ3nBfk8mAOu/B6v7FMJ+NHTIgUPi7rj+4nv4= -github.com/Masterminds/semver/v3 v3.3.1/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM= -github.com/Masterminds/sprig/v3 v3.3.0 h1:mQh0Yrg1XPo6vjYXgtf5OtijNAKJRNcTdOOGZe3tPhs= -github.com/Masterminds/sprig/v3 v3.3.0/go.mod h1:Zy1iXRYNqNLUolqCpL4uhk6SHUMAOSCzdgBfDb35Lz0= -github.com/Masterminds/squirrel v1.5.4 h1:uUcX/aBc8O7Fg9kaISIUsHXdKuqehiXAMQTYX8afzqM= -github.com/Masterminds/squirrel v1.5.4/go.mod h1:NNaOrjSoIDfDA40n7sr2tPNZRfjzjA400rg+riTZj10= github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY= github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU= -github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio= -github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs= -github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 h1:DklsrG3dyBCFEj5IhUbnKptjxatkF07cF2ak3yi77so= -github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2/go.mod h1:WaHUgvxTVq04UNunO+XhnAqY/wQc+bxr74GqbsZ/Jqw= -github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= -github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= -github.com/blang/semver/v4 v4.0.0 h1:1PFHFE6yCCTv8C1TeyNNarDzntLi7wMI5i/pzqYIsAM= -github.com/blang/semver/v4 v4.0.0/go.mod h1:IbckMUScFkM3pff0VJDNKRiT6TG/YpiHIM2yvyW5YoQ= -github.com/bshuster-repo/logrus-logstash-hook v1.0.0 h1:e+C0SB5R1pu//O4MQ3f9cFuPGoOVeF2fE4Og9otCc70= -github.com/bshuster-repo/logrus-logstash-hook v1.0.0/go.mod h1:zsTqEiSzDgAa/8GZR7E1qaXrhYNDKBYy5/dWPTIflbk= github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8= github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= -github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= -github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= -github.com/chai2010/gettext-go v1.0.2 h1:1Lwwip6Q2QGsAdl/ZKPCwTe9fe0CjlUbqj5bFNSjIRk= -github.com/chai2010/gettext-go v1.0.2/go.mod h1:y+wnP2cHYaVj19NZhYKAwEMH2CI1gNHeQQ+5AjwawxA= -github.com/cloudnative-pg/barman-cloud v0.3.1 h1:kzkY77k2lN/caoyh7ibXDSZjJeSJTNvnVt6Gfa8Iq5M= -github.com/cloudnative-pg/barman-cloud v0.3.1/go.mod h1:4HL3AjY9oEl2Ed0HSkyvTZEQPhwyFOaAnuCz9lfVeYQ= -github.com/cloudnative-pg/cloudnative-pg v1.26.0 h1:X+ayWg6TXqglziSPejKiviPTf/CSh4AmYXx1mrS3i8s= -github.com/cloudnative-pg/cloudnative-pg v1.26.0/go.mod h1:aNh0g7UHKxyka4HYOqLaYDWtZ6tORQ7HbIHLvb79sx8= -github.com/cloudnative-pg/machinery v0.2.0 h1:x8OAwxdeL/6wkbxqorz+nX6UovTyx7/TBeCfiRebR2o= -github.com/cloudnative-pg/machinery v0.2.0/go.mod h1:Kg8W8Tb/1UFGGtw3hR8S5SytSWddlHaCnJSgBo4x/nc= -github.com/containerd/containerd v1.7.27 h1:yFyEyojddO3MIGVER2xJLWoCIn+Up4GaHFquP7hsFII= -github.com/containerd/containerd v1.7.27/go.mod h1:xZmPnl75Vc+BLGt4MIfu6bp+fy03gdHAn9bz+FreFR0= github.com/containerd/errdefs v1.0.0 h1:tg5yIfIlQIrxYtu9ajqY42W3lpS19XqdxRQeEwYG8PI= github.com/containerd/errdefs v1.0.0/go.mod h1:+YBYIdtsnF4Iw6nWZhJcqGSg/dwvV7tyJ/kCkyJ2k+M= github.com/containerd/errdefs/pkg v0.3.0 h1:9IKJ06FvyNlexW690DXuQNx2KA2cUJXx151Xdx3ZPPE= github.com/containerd/errdefs/pkg v0.3.0/go.mod h1:NJw6s9HwNuRhnjJhM7pylWwMyAkmCQvQ4GpJHEqRLVk= github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I= github.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo= -github.com/containerd/platforms v0.2.1 h1:zvwtM3rz2YHPQsF2CHYM8+KtB5dvhISiXh5ZpSBQv6A= -github.com/containerd/platforms v0.2.1/go.mod h1:XHCb+2/hzowdiut9rkudds9bE5yJ7npe7dG/wG+uFPw= -github.com/coreos/go-systemd/v22 v22.5.0 h1:RrqgGjYQKalulkV8NGVIfkXQf6YYmOyiJKk8iXXhfZs= -github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= -github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= -github.com/creack/pty v1.1.18 h1:n56/Zwd5o6whRC5PMGretI4IdRLlmBXYNjScPaBgsbY= -github.com/creack/pty v1.1.18/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4= -github.com/cyphar/filepath-securejoin v0.4.1 h1:JyxxyPEaktOD+GAnqIqTf9A8tHyAG22rowi7HkoSU1s= -github.com/cyphar/filepath-securejoin v0.4.1/go.mod h1:Sdj7gXlvMcPZsbhwhQ33GguGLDGQL7h7bg04C/+u9jI= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78= -github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc= -github.com/distribution/distribution/v3 v3.0.0 h1:q4R8wemdRQDClzoNNStftB2ZAfqOiN6UX90KJc4HjyM= -github.com/distribution/distribution/v3 v3.0.0/go.mod h1:tRNuFoZsUdyRVegq8xGNeds4KLjwLCRin/tTo6i1DhU= github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk= github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E= -github.com/docker/docker v28.2.2+incompatible h1:CjwRSksz8Yo4+RmQ339Dp/D2tGO5JxwYeqtMOEe0LDw= -github.com/docker/docker v28.2.2+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= -github.com/docker/docker-credential-helpers v0.9.3 h1:gAm/VtF9wgqJMoxzT3Gj5p4AqIjCBS4wrsOh9yRqcz8= -github.com/docker/docker-credential-helpers v0.9.3/go.mod h1:x+4Gbw9aGmChi3qTLZj8Dfn0TD20M/fuWy0E5+WDeCo= +github.com/docker/docker v28.3.3+incompatible h1:Dypm25kh4rmk49v1eiVbsAtpAsYURjYkaKubwuBdxEI= +github.com/docker/docker v28.3.3+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= github.com/docker/go-connections v0.5.0 h1:USnMq7hx7gwdVZq1L49hLXaFtUdTADjXGp+uj1Br63c= github.com/docker/go-connections v0.5.0/go.mod h1:ov60Kzw0kKElRwhNs9UlUHAE/F9Fe6GLaXnqyDdmEXc= -github.com/docker/go-events v0.0.0-20190806004212-e31b211e4f1c h1:+pKlWGMw7gf6bQ+oDZB4KHQFypsfjYlq/C4rfL7D3g8= -github.com/docker/go-events v0.0.0-20190806004212-e31b211e4f1c/go.mod h1:Uw6UezgYA44ePAFQYUehOuCzmy5zmg/+nl2ZfMWGkpA= -github.com/docker/go-metrics v0.0.1 h1:AgB/0SvBxihN0X8OR4SjsblXkbMvalQ8cjmtKQ2rQV8= -github.com/docker/go-metrics v0.0.1/go.mod h1:cG1hvH2utMXtqgqqYE9plW6lDxS3/5ayHzueweSI3Vw= github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4= github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= -github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= -github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= -github.com/emicklei/go-restful/v3 v3.12.2 h1:DhwDP0vY3k8ZzE0RunuJy8GhNpPL6zqLkDf9B/a0/xU= -github.com/emicklei/go-restful/v3 v3.12.2/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= -github.com/evanphx/json-patch v5.9.11+incompatible h1:ixHHqfcGvxhWkniF1tWxBHA0yb4Z+d1UQi45df52xW8= -github.com/evanphx/json-patch v5.9.11+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk= -github.com/evanphx/json-patch/v5 v5.9.11 h1:/8HVnzMq13/3x9TPvjG08wUGqBTmZBsCWzjTM0wiaDU= -github.com/evanphx/json-patch/v5 v5.9.11/go.mod h1:3j+LviiESTElxA4p3EMKAB9HXj3/XEtnUf6OZxqIQTM= -github.com/exponent-io/jsonpath v0.0.0-20210407135951-1de76d718b3f h1:Wl78ApPPB2Wvf/TIe2xdyJxTlb6obmF18d8QdkxNDu4= -github.com/exponent-io/jsonpath v0.0.0-20210407135951-1de76d718b3f/go.mod h1:OSYXu++VVOHnXeitef/D8n/6y4QV8uLHSFXX4NeXMGc= -github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM= -github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU= github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= -github.com/foxcpp/go-mockdns v1.1.0 h1:jI0rD8M0wuYAxL7r/ynTrCQQq0BVqfB99Vgk7DlmewI= -github.com/foxcpp/go-mockdns v1.1.0/go.mod h1:IhLeSFGed3mJIAXPH2aiRQB+kqz7oqu8ld2qVbOu7Wk= -github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= -github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= -github.com/fsnotify/fsnotify v1.8.0 h1:dAwr6QBTBZIkG8roQaJjGof0pp0EeF+tNV7YBP3F/8M= -github.com/fsnotify/fsnotify v1.8.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0= -github.com/fxamacker/cbor/v2 v2.7.0 h1:iM5WgngdRBanHcxugY4JySA0nk1wZorNOpTgCMedv5E= -github.com/fxamacker/cbor/v2 v2.7.0/go.mod h1:pxXPTn3joSm21Gbwsv0w9OSA2y1HFR9qXEeXQVeNoDQ= -github.com/go-errors/errors v1.5.1 h1:ZwEMSLRCapFLflTpT7NKaAc7ukJ8ZPEjzlxt8rPN8bk= -github.com/go-errors/errors v1.5.1/go.mod h1:sIVyrIiJhuEF+Pj9Ebtd6P/rEYROXFi3BopGUQ5a5Og= -github.com/go-gorp/gorp/v3 v3.1.0 h1:ItKF/Vbuj31dmV4jxA1qblpSwkl9g1typ24xoe70IGs= -github.com/go-gorp/gorp/v3 v3.1.0/go.mod h1:dLEjIyyRNiXvNZ8PSmzpt1GsWAUK8kjVhEpjH8TixEw= -github.com/go-ini/ini v1.67.0 h1:z6ZrTEZqSWOTyH2FlglNbNgARyHG8oLW9gMELqKr06A= -github.com/go-ini/ini v1.67.0/go.mod h1:ByCAeIL28uOIIG0E3PJtZPDL8WnHpFKFOtgjp+3Ies8= github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= -github.com/go-logr/zapr v1.3.0 h1:XGdV8XW8zdwFiwOA2Dryh1gj2KRQyOOoNmBy4EplIcQ= -github.com/go-logr/zapr v1.3.0/go.mod h1:YKepepNBd1u/oyhd/yQmtjVXmm9uML4IXUgMOwR8/Gg= -github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0= -github.com/go-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE= -github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78= -github.com/go-openapi/jsonpointer v0.21.1 h1:whnzv/pNXtK2FbX/W9yJfRmE2gsmkfahjMKB0fZvcic= -github.com/go-openapi/jsonpointer v0.21.1/go.mod h1:50I1STOfbY1ycR8jGz8DaMeLCdXiI6aDteEdRNNzpdk= -github.com/go-openapi/jsonreference v0.21.0 h1:Rs+Y7hSXT83Jacb7kFyjn4ijOuVGSvOdF2+tg1TRrwQ= -github.com/go-openapi/jsonreference v0.21.0/go.mod h1:LmZmgsrTkVg9LG4EaHeY8cBDslNPMo06cago5JNLkm4= -github.com/go-openapi/swag v0.23.1 h1:lpsStH0n2ittzTnbaSloVZLuB5+fvSY/+hnagBjSNZU= -github.com/go-openapi/swag v0.23.1/go.mod h1:STZs8TbRvEQQKUA+JZNAm3EWlgaOBGpyFDqQnDHMef0= -github.com/go-sql-driver/mysql v1.8.1 h1:LedoTUt/eveggdHS9qUFC1EFSa8bU2+1pZjSRpvNJ1Y= -github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg= -github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI= -github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8= -github.com/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y= -github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8= -github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4= -github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M= github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= -github.com/golang-jwt/jwt/v4 v4.5.2 h1:YtQM7lnr8iZ+j5q71MGKkNw9Mn7AjHM68uc9g5fXeUI= -github.com/golang-jwt/jwt/v4 v4.5.2/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0= -github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= -github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= -github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= -github.com/google/btree v1.1.3 h1:CVpQJjYgC4VbzxeGVHfvZrv1ctoYCAI8vbl07Fcxlyg= -github.com/google/btree v1.1.3/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl761Gm4= -github.com/google/gnostic-models v0.6.9 h1:MU/8wDLif2qCXZmzncUQ/BOfxWfthHi63KqpoNbWqVw= -github.com/google/gnostic-models v0.6.9/go.mod h1:CiWsm0s6BSQd1hRn8/QmxqB6BesYcbSZxsz9b0KuDBw= github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= -github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= -github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0= -github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= -github.com/google/pprof v0.0.0-20250403155104-27863c87afa6 h1:BHT72Gu3keYf3ZEu2J0b1vyeLSOYI8bm5wbJM/8yDe8= -github.com/google/pprof v0.0.0-20250403155104-27863c87afa6/go.mod h1:boTsfXsheKC2y+lKOCMpSfarhxDeIzfZG1jqGcPl3cA= -github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 h1:El6M4kTTCOh6aBiKaUGG7oYTSPP8MxqL4YI3kZKwcP4= -github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod h1:pupxD2MaaD3pAXIBCelhxNneeOaAeabZDe5s4K6zSpQ= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/gorilla/handlers v1.5.2 h1:cLTUSsNkgcwhgRqvCNmdbRWG0A3N4F+M2nWKdScwyEE= -github.com/gorilla/handlers v1.5.2/go.mod h1:dX+xVpaxdSw+q0Qek8SSsl3dfMk3jNddUkMzo0GtH0w= -github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY= -github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ= -github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674 h1:JeSE6pjso5THxAzdVpqr6/geYxZytqFMBCOtn/ujyeo= -github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674/go.mod h1:r4w70xmWCQKmi1ONH4KIaBptdivuRPyosB9RmPlGEwA= -github.com/gosuri/uitable v0.0.4 h1:IG2xLKRvErL3uhY6e1BylFzG+aJiwQviDDTfOKeKTpY= -github.com/gosuri/uitable v0.0.4/go.mod h1:tKR86bXuXPZazfOTG1FIzvjIdXzd0mo4Vtn16vt0PJo= -github.com/gregjones/httpcache v0.0.0-20190611155906-901d90724c79 h1:+ngKgrYPPJrOjhax5N+uePQ0Fh1Z7PheYoUI/0nzkPA= -github.com/gregjones/httpcache v0.0.0-20190611155906-901d90724c79/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA= -github.com/grpc-ecosystem/grpc-gateway v1.16.0 h1:gmcG1KaJ57LophUzW0Hy8NmPhnMZb4M0+kPpLofRdBo= github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.3 h1:5ZPtiqj0JL5oKWmcsq4VMaAW5ukBEgSGXEN89zeH1Jo= github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.3/go.mod h1:ndYquD05frm2vACXE1nsccT4oJzjhw2arTS2cpUD1PI= -github.com/h2non/parth v0.0.0-20190131123155-b4df798d6542 h1:2VTzZjLZBgl62/EtslCrtky5vbi9dd7HrQPQIx6wqiw= -github.com/h2non/parth v0.0.0-20190131123155-b4df798d6542/go.mod h1:Ow0tF8D4Kplbc8s8sSb3V2oUCygFHVp8gC3Dn6U4MNI= -github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= -github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I= -github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= -github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo= -github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM= github.com/hashicorp/go-set/v3 v3.0.0 h1:CaJBQvQCOWoftrBcDt7Nwgo0kdpmrKxar/x2o6pV9JA= github.com/hashicorp/go-set/v3 v3.0.0/go.mod h1:IEghM2MpE5IaNvL+D7X480dfNtxjRXZ6VMpK3C8s2ok= -github.com/hashicorp/golang-lru v0.6.0 h1:uL2shRDx7RTrOrTCUZEGP/wJUFiUI8QT6E7z5o8jga4= -github.com/hashicorp/golang-lru/arc/v2 v2.0.5 h1:l2zaLDubNhW4XO3LnliVj0GXO3+/CGNJAg1dcN2Fpfw= -github.com/hashicorp/golang-lru/arc/v2 v2.0.5/go.mod h1:ny6zBSQZi2JxIeYcv7kt2sH2PXJtirBN7RDhRpxPkxU= -github.com/hashicorp/golang-lru/v2 v2.0.5 h1:wW7h1TG88eUIJ2i69gaE3uNVtEPIagzhGvHgwfx2Vm4= -github.com/hashicorp/golang-lru/v2 v2.0.5/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM= -github.com/huandu/xstrings v1.5.0 h1:2ag3IFq9ZDANvthTwTiqSSZLjDc+BedvHPAp5tJy2TI= -github.com/huandu/xstrings v1.5.0/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE= -github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= -github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= -github.com/jmoiron/sqlx v1.4.0 h1:1PLqN7S1UYp5t4SrVVnt4nUVNemrDAtxlulVe+Qgm3o= -github.com/jmoiron/sqlx v1.4.0/go.mod h1:ZrZ7UsYB/weZdl2Bxg6jCRO9c3YHl8r3ahlKmRT4JLY= -github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= -github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= -github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= -github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= -github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo= -github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ= -github.com/klauspost/cpuid/v2 v2.0.1/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= -github.com/klauspost/cpuid/v2 v2.2.10 h1:tBs3QSyvjDyFTq3uoc/9xFpCuOsJQFNPiAhYdw2skhE= -github.com/klauspost/cpuid/v2 v2.2.10/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0= -github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= -github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= -github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= -github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= -github.com/kubernetes-csi/external-snapshotter/client/v8 v8.2.0 h1:Q3jQ1NkFqv5o+F8dMmHd8SfEmlcwNeo1immFApntEwE= -github.com/kubernetes-csi/external-snapshotter/client/v8 v8.2.0/go.mod h1:E3vdYxHj2C2q6qo8/Da4g7P+IcwqRZyy3gJBzYybV9Y= -github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= -github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= -github.com/lann/builder v0.0.0-20180802200727-47ae307949d0 h1:SOEGU9fKiNWd/HOJuq6+3iTQz8KNCLtVX6idSoTLdUw= -github.com/lann/builder v0.0.0-20180802200727-47ae307949d0/go.mod h1:dXGbAdH5GtBTC4WfIxhKZfyBF/HBFgRZSWwZ9g/He9o= -github.com/lann/ps v0.0.0-20150810152359-62de8c46ede0 h1:P6pPBnrTSX3DEVR4fDembhRWSsG5rVo6hYhAB/ADZrk= -github.com/lann/ps v0.0.0-20150810152359-62de8c46ede0/go.mod h1:vmVJ0l/dxyfGW6FmdpVm2joNMFikkuWg0EoCKLGUMNw= -github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw= -github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= -github.com/liggitt/tabwriter v0.0.0-20181228230101-89fcab3d43de h1:9TO3cAIGXtEhnIaL+V+BEER86oLrvS+kWobKpbJuye0= -github.com/liggitt/tabwriter v0.0.0-20181228230101-89fcab3d43de/go.mod h1:zAbeS9B/r2mtpb6U+EI2rYA5OAXxsYw6wTamcNW+zcE= -github.com/lufia/plan9stats v0.0.0-20250317134145-8bc96cf8fc35 h1:PpXWgLPs+Fqr325bN2FD2ISlRRztXibcX6e8f5FR5Dc= -github.com/lufia/plan9stats v0.0.0-20250317134145-8bc96cf8fc35/go.mod h1:autxFIvghDt3jPTLoqZ9OZ7s9qTGNAWmYCjVFWPX/zg= -github.com/mailru/easyjson v0.9.0 h1:PrnmzHw7262yW8sTBwxi1PdJA3Iw/EKBa8psRf7d9a4= -github.com/mailru/easyjson v0.9.0/go.mod h1:1+xMtQp2MRNVL/V1bOzuP3aP8VNwRW55fQUto+XFtTU= github.com/matrix-org/complement v0.0.0-20251120181401-44111a2a8a9d h1:s2Xc9GB2E/pXdElP18h8+04Y3SmhaII7xh2YmCM7oZc= github.com/matrix-org/complement v0.0.0-20251120181401-44111a2a8a9d/go.mod h1:HioTV089DHLBfljH9QLGifJRE4Avnyk08BXXhCwd4gs= github.com/matrix-org/gomatrix v0.0.0-20220926102614-ceba4d9f7530 h1:kHKxCOLcHH8r4Fzarl4+Y3K5hjothkVW5z7T1dUM11U= @@ -231,148 +50,35 @@ github.com/matrix-org/gomatrixserverlib v0.0.0-20250813150445-9f5070a65744 h1:5G github.com/matrix-org/gomatrixserverlib v0.0.0-20250813150445-9f5070a65744/go.mod h1:b6KVfDjXjA5Q7vhpOaMqIhFYvu5BuFVZixlNeTV/CLc= github.com/matrix-org/util v0.0.0-20221111132719-399730281e66 h1:6z4KxomXSIGWqhHcfzExgkH3Z3UkIXry4ibJS4Aqz2Y= github.com/matrix-org/util v0.0.0-20221111132719-399730281e66/go.mod h1:iBI1foelCqA09JJgPV0FYz4qA5dUXYOxMi57FxKBdd4= -github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE= -github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8= -github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= -github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= -github.com/mattn/go-runewidth v0.0.15 h1:UNAjwbU9l54TA3KzvqLGxwWjHmMgBUVhBiTjelZgg3U= -github.com/mattn/go-runewidth v0.0.15/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= -github.com/mattn/go-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU= -github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= -github.com/matttproud/golang_protobuf_extensions v1.0.4 h1:mmDVorXM7PCGKw94cs5zkfA9PSy5pEvNWRP0ET0TIVo= -github.com/matttproud/golang_protobuf_extensions v1.0.4/go.mod h1:BSXmuO+STAnVfrANrmjBb36TMTDstsz7MSK+HVaYKv4= -github.com/miekg/dns v1.1.66 h1:FeZXOS3VCVsKnEAd+wBkjMC3D2K+ww66Cq3VnCINuJE= -github.com/miekg/dns v1.1.66/go.mod h1:jGFzBsSNbJw6z1HYut1RKBKHA9PBdxeHrZG8J+gC2WE= -github.com/minio/crc64nvme v1.0.1 h1:DHQPrYPdqK7jQG/Ls5CTBZWeex/2FMS3G5XGkycuFrY= -github.com/minio/crc64nvme v1.0.1/go.mod h1:eVfm2fAzLlxMdUGc0EEBGSMmPwmXD5XiNRpnu9J3bvg= -github.com/minio/madmin-go/v3 v3.0.100 h1:1gsy1kHgk8v9ZQyEU/L9k1ZoImFid+hlzwrOgUULB88= -github.com/minio/madmin-go/v3 v3.0.100/go.mod h1:pMLdj9OtN0CANNs5tdm6opvOlDFfj0WhbztboZAjRWE= -github.com/minio/md5-simd v1.1.2 h1:Gdi1DZK69+ZVMoNHRXJyNcxrMA4dSxoYHZSQbirFg34= -github.com/minio/md5-simd v1.1.2/go.mod h1:MzdKDxYpY2BT9XQFocsiZf/NKVtR7nkE4RoEpN+20RM= -github.com/minio/minio-go/v7 v7.0.89 h1:hx4xV5wwTUfyv8LarhJAwNecnXpoTsj9v3f3q/ZkiJU= -github.com/minio/minio-go/v7 v7.0.89/go.mod h1:2rFnGAp02p7Dddo1Fq4S2wYOfpF0MUTSeLTRC90I204= -github.com/minio/operator v0.0.0-20250423184541-6eee6a7caa70 h1:CTpACn2vNMA76TXKGufGhxVa9KIw3Qdvtx6j/l5fKeE= -github.com/minio/operator v0.0.0-20250423184541-6eee6a7caa70/go.mod h1:NL8ANdLUtI70DmG2x2EMCfoe6d3ijD1Xok/N1Tr8Czo= -github.com/mitchellh/copystructure v1.2.0 h1:vpKXTN4ewci03Vljg/q9QvCGUDttBOGBIa15WveJJGw= -github.com/mitchellh/copystructure v1.2.0/go.mod h1:qLl+cE2AmVv+CoeAwDPye/v+N2HKCj9FbZEVFJRxO9s= -github.com/mitchellh/go-wordwrap v1.0.1 h1:TLuKupo69TCn6TQSyGxwI1EblZZEsQ0vMlAFQflz0v0= -github.com/mitchellh/go-wordwrap v1.0.1/go.mod h1:R62XHJLzvMFRBbcrT7m7WgmE1eOyTSsCt+hzestvNj0= -github.com/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zxSIeXaQ= -github.com/mitchellh/reflectwalk v1.0.2/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw= -github.com/mittwald/go-helm-client v0.12.18 h1:i9cJNv/YC3ZPKUKVNYTlrOO7ZO6YFKE/ak3J5TeYHPU= -github.com/mittwald/go-helm-client v0.12.18/go.mod h1:dLl5NkdKCvwKvLIdZzg4MDbxhSKmuimdmM3WpsAzS0I= github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0= github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo= -github.com/moby/spdystream v0.5.0 h1:7r0J1Si3QO/kjRitvSLVVFUjxMEb/YLj6S9FF62JBCU= -github.com/moby/spdystream v0.5.0/go.mod h1:xBAYlnt/ay+11ShkdFKNAG7LsyK/tmNBVvVOwrfMgdI= github.com/moby/sys/atomicwriter v0.1.0 h1:kw5D/EqkBwsBFi0ss9v1VG3wIkVhzGvLklJ+w3A14Sw= github.com/moby/sys/atomicwriter v0.1.0/go.mod h1:Ul8oqv2ZMNHOceF643P6FKPXeCmYtlQMvpizfsSoaWs= github.com/moby/sys/sequential v0.6.0 h1:qrx7XFUd/5DxtqcoH1h438hF5TmOvzC/lspjy7zgvCU= github.com/moby/sys/sequential v0.6.0/go.mod h1:uyv8EUTrca5PnDsdMGXhZe6CCe8U/UiTWd+lL+7b/Ko= github.com/moby/term v0.5.2 h1:6qk3FJAFDs6i/q3W/pQ97SX192qKfZgGjCQqfCJkgzQ= github.com/moby/term v0.5.2/go.mod h1:d3djjFCrjnB+fl8NJux+EJzu0msscUP+f8it8hPkFLc= -github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= -github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= -github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= -github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= -github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= -github.com/monochromegane/go-gitignore v0.0.0-20200626010858-205db1a8cc00 h1:n6/2gBQ3RWajuToeY6ZtZTIKv2v7ThUy5KKusIT0yc0= -github.com/monochromegane/go-gitignore v0.0.0-20200626010858-205db1a8cc00/go.mod h1:Pm3mSP3c5uWn86xMLZ5Sa7JB9GsEZySvHYXCTK4E9q4= github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A= github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc= -github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= -github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= -github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f h1:y5//uYreIhSUg3J1GEMiLbxo1LJaP8RfCpH6pymGZus= -github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f/go.mod h1:ZdcZmHo+o7JKHSa8/e818NopupXU1YMK5fe1lsApnBw= github.com/oleiade/lane/v2 v2.0.0 h1:XW/ex/Inr+bPkLd3O240xrFOhUkTd4Wy176+Gv0E3Qw= github.com/oleiade/lane/v2 v2.0.0/go.mod h1:i5FBPFAYSWCgLh58UkUGCChjcCzef/MI7PlQm2TKCeg= -github.com/onsi/ginkgo/v2 v2.23.4 h1:ktYTpKJAVZnDT4VjxSbiBenUjmlL/5QkBEocaWXiQus= -github.com/onsi/ginkgo/v2 v2.23.4/go.mod h1:Bt66ApGPBFzHyR+JO10Zbt0Gsp4uWxu5mIOTusL46e8= -github.com/onsi/gomega v1.37.0 h1:CdEG8g0S133B4OswTDC/5XPSzE1OeP29QOioj2PID2Y= -github.com/onsi/gomega v1.37.0/go.mod h1:8D9+Txp43QWKhM24yyOBEdpkzN8FvJyAwecBgsU4KU0= github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= github.com/opencontainers/image-spec v1.1.1 h1:y0fUlFfIZhPF1W537XOLg0/fcx6zcHCJwooC2xJA040= github.com/opencontainers/image-spec v1.1.1/go.mod h1:qpqAh3Dmcf36wStyyWU+kCeDgrGnAve2nCC8+7h8Q0M= -github.com/peterbourgon/diskv v2.0.1+incompatible h1:UBdAOUP5p4RWqPBg048CAvpKN+vxiaj6gdUUzhl4XmI= -github.com/peterbourgon/diskv v2.0.1+incompatible/go.mod h1:uqqh8zWWbv1HBMNONnaR/tNboyR3/BZd58JJSHlUSCU= -github.com/phayes/freeport v0.0.0-20220201140144-74d24b5ae9f5 h1:Ii+DKncOVM8Cu1Hc+ETb5K+23HdAMvESYE3ZJ5b5cMI= -github.com/phayes/freeport v0.0.0-20220201140144-74d24b5ae9f5/go.mod h1:iIss55rKnNBTvrwdmkUpLnDpZoAHvWaiq5+iMmen4AE= -github.com/philhofer/fwd v1.1.3-0.20240916144458-20a13a1f6b7c h1:dAMKvw0MlJT1GshSTtih8C2gDs04w8dReiOGXrGLNoY= -github.com/philhofer/fwd v1.1.3-0.20240916144458-20a13a1f6b7c/go.mod h1:RqIHx9QI14HlwKwm98g9Re5prTQ6LdeRQn+gXJFxsJM= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 h1:o4JXh1EVt9k/+g42oCprj/FisM4qX9L3sZB3upGN2ZU= -github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE= -github.com/poy/onpar v1.1.2 h1:QaNrNiZx0+Nar5dLgTVp5mXkyoVFIbepjyEoGSnhbAY= -github.com/poy/onpar v1.1.2/go.mod h1:6X8FLNoxyr9kkmnlqpK6LSoiOtrO6MICtWwEuWkLjzg= -github.com/prometheus-operator/prometheus-operator/pkg/apis/monitoring v0.80.1 h1:DP+PUNVOc+Bkft8a4QunLzaZ0RspWuD3tBbcPHr2PeE= -github.com/prometheus-operator/prometheus-operator/pkg/apis/monitoring v0.80.1/go.mod h1:6x4x0t9BP35g4XcjkHE9EB3RxhyfxpdpmZKd/Qyk8+M= -github.com/prometheus/client_golang v1.22.0 h1:rb93p9lokFEsctTys46VnV1kLCDpVZ0a/Y92Vm0Zc6Q= -github.com/prometheus/client_golang v1.22.0/go.mod h1:R7ljNsLXhuQXYZYtw6GAE9AZg8Y7vEW5scdCXrWRXC0= -github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk= -github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE= -github.com/prometheus/common v0.65.0 h1:QDwzd+G1twt//Kwj/Ww6E9FQq1iVMmODnILtW1t2VzE= -github.com/prometheus/common v0.65.0/go.mod h1:0gZns+BLRQ3V6NdaerOhMbwwRbNh9hkGINtQAsP5GS8= -github.com/prometheus/procfs v0.16.0 h1:xh6oHhKwnOJKMYiYBDWmkHqQPyiY40sny36Cmx2bbsM= -github.com/prometheus/procfs v0.16.0/go.mod h1:8veyXUu3nGP7oaCxhX6yeaM5u4stL2FeMXnCqhDthZg= -github.com/prometheus/prom2json v1.4.1 h1:7McxdrHgPEOtMwWjkKtd0v5AhpR2Q6QAnlHKVxq0+tQ= -github.com/prometheus/prom2json v1.4.1/go.mod h1:CzOQykSKFxXuC7ELUZHOHQvwKesQ3eN0p2PWLhFitQM= -github.com/prometheus/prometheus v0.302.1 h1:xqVdrwrB4WNpdgJqxsz5loqFWNUZitsK8myqLuSZ6Ag= -github.com/prometheus/prometheus v0.302.1/go.mod h1:YcyCoTbUR/TM8rY3Aoeqr0AWTu/pu1Ehh+trpX3eRzg= -github.com/redis/go-redis/extra/rediscmd/v9 v9.0.5 h1:EaDatTxkdHG+U3Bk4EUr+DZ7fOGwTfezUiUJMaIcaho= -github.com/redis/go-redis/extra/rediscmd/v9 v9.0.5/go.mod h1:fyalQWdtzDBECAQFBJuQe5bzQ02jGd5Qcbgb97Flm7U= -github.com/redis/go-redis/extra/redisotel/v9 v9.0.5 h1:EfpWLLCyXw8PSM2/XNJLjI3Pb27yVE+gIAfeqp8LUCc= -github.com/redis/go-redis/extra/redisotel/v9 v9.0.5/go.mod h1:WZjPDy7VNzn77AAfnAfVjZNvfJTYfPetfZk5yoSTLaQ= -github.com/redis/go-redis/v9 v9.7.3 h1:YpPyAayJV+XErNsatSElgRZZVCwXX9QzkKYNvO7x0wM= -github.com/redis/go-redis/v9 v9.7.3/go.mod h1:bGUrSggJ9X9GUmZpZNEOQKaANxSGgOEBRltRTZHSvrA= -github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= -github.com/rivo/uniseg v0.4.4 h1:8TfxU8dW6PdqD27gjM8MVNuicgxIjxpm4K7x4jp8sis= -github.com/rivo/uniseg v0.4.4/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= -github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII= -github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o= -github.com/rs/xid v1.6.0 h1:fV591PaemRlL6JfRxGDEPl69wICngIQ3shQtzfy2gxU= -github.com/rs/xid v1.6.0/go.mod h1:7XoLgs4eV+QndskICGsho+ADou8ySMSjJKDIan90Nz0= -github.com/rubenv/sql-migrate v1.8.0 h1:dXnYiJk9k3wetp7GfQbKJcPHjVJL6YK19tKj8t2Ns0o= -github.com/rubenv/sql-migrate v1.8.0/go.mod h1:F2bGFBwCU+pnmbtNYDeKvSuvL6lBVtXDXUUv5t+u1qw= -github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= -github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= -github.com/safchain/ethtool v0.5.10 h1:Im294gZtuf4pSGJRAOGKaASNi3wMeFaGaWuSaomedpc= -github.com/safchain/ethtool v0.5.10/go.mod h1:w9jh2Lx7YBR4UwzLkzCmWl85UY0W2uZdd7/DckVE5+c= -github.com/secure-io/sio-go v0.3.1 h1:dNvY9awjabXTYGsTF1PiCySl9Ltofk9GA3VdWlo7rRc= -github.com/secure-io/sio-go v0.3.1/go.mod h1:+xbkjDzPjwh4Axd07pRKSNriS9SCiYksWnZqdnfpQxs= -github.com/sergi/go-diff v1.2.0 h1:XU+rvMAioB0UC3q1MFrIQy4Vo5/4VsRDQQXHsEya6xQ= -github.com/sergi/go-diff v1.2.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM= -github.com/shirou/gopsutil/v3 v3.24.5 h1:i0t8kL+kQTvpAYToeuiVk3TgDeKOFioZO3Ztz/iZ9pI= -github.com/shirou/gopsutil/v3 v3.24.5/go.mod h1:bsoOS1aStSs9ErQ1WWfxllSeS1K5D+U30r2NfcubMVk= -github.com/shoenig/go-m1cpu v0.1.6 h1:nxdKQNcEB6vzgA2E2bvzKIYRuNj7XNJ4S/aRSwKzFtM= -github.com/shoenig/go-m1cpu v0.1.6/go.mod h1:1JJMcUBvfNwpq05QDQVAnx3gUHr9IYF7GNg9SUEw2VQ= github.com/shoenig/test v1.11.0 h1:NoPa5GIoBwuqzIviCrnUJa+t5Xb4xi5Z+zODJnIDsEQ= github.com/shoenig/test v1.11.0/go.mod h1:UxJ6u/x2v/TNs/LoLxBNJRV9DiwBBKYxXSyczsBHFoI= -github.com/shopspring/decimal v1.4.0 h1:bxl37RwXBklmTi0C79JfXCEBD1cqqHt0bbgBAGFp81k= -github.com/shopspring/decimal v1.4.0/go.mod h1:gawqmDU56v4yIKSwfBSFip1HdCCXN8/+DMd9qYNcwME= github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= -github.com/spf13/cast v1.7.0 h1:ntdiHjuueXFgm5nzDRdOS4yfT43P5Fnud6DH50rz/7w= -github.com/spf13/cast v1.7.0/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo= -github.com/spf13/cobra v1.9.1 h1:CXSaggrXdbHK9CF+8ywj8Amf7PBRmPCOJugH954Nnlo= -github.com/spf13/cobra v1.9.1/go.mod h1:nDyEzZ8ogv936Cinf6g1RU9MRY64Ir93oCnqb9wxYW0= -github.com/spf13/pflag v1.0.6 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o= -github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= -github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= -github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= -github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= -github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= -github.com/thoas/go-funk v0.9.3 h1:7+nAEx3kn5ZJcnDm2Bh23N2yOtweO14bi//dvRtgLpw= -github.com/thoas/go-funk v0.9.3/go.mod h1:+IWnUfUmFO1+WVYQWQtIJHeRRdaIyyYglZN7xzUPe4Q= github.com/tidwall/gjson v1.14.2/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= github.com/tidwall/gjson v1.18.0 h1:FIDeeyB800efLX89e5a8Y0BNH+LOngJyGrIWxG2FKQY= github.com/tidwall/gjson v1.18.0/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= @@ -383,84 +89,30 @@ github.com/tidwall/pretty v1.2.1 h1:qjsOFOWWQl+N3RsoF5/ssm1pHmJJwhjlSbZ51I6wMl4= github.com/tidwall/pretty v1.2.1/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= github.com/tidwall/sjson v1.2.5 h1:kLy8mja+1c9jlljvWTlSazM7cKDRfJuR/bOJhcY5NcY= github.com/tidwall/sjson v1.2.5/go.mod h1:Fvgq9kS/6ociJEDnK0Fk1cpYF4FIW6ZF7LAe+6jwd28= -github.com/tinylib/msgp v1.2.5 h1:WeQg1whrXRFiZusidTQqzETkRpGjFjcIhW6uqWH09po= -github.com/tinylib/msgp v1.2.5/go.mod h1:ykjzy2wzgrlvpDCRc4LA8UXy6D8bzMSuAF3WD57Gok0= -github.com/tklauser/go-sysconf v0.3.15 h1:VE89k0criAymJ/Os65CSn1IXaol+1wrsFHEB8Ol49K4= -github.com/tklauser/go-sysconf v0.3.15/go.mod h1:Dmjwr6tYFIseJw7a3dRLJfsHAMXZ3nEnL/aZY+0IuI4= -github.com/tklauser/numcpus v0.10.0 h1:18njr6LDBk1zuna922MgdjQuJFjrdppsZG60sHGfjso= -github.com/tklauser/numcpus v0.10.0/go.mod h1:BiTKazU708GQTYF4mB+cmlpT2Is1gLk7XVuEeem8LsQ= -github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= -github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= -github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU= -github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb h1:zGWFAtiMcyryUHoUjUJX0/lt1H2+i2Ka2n+D3DImSNo= -github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU= -github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 h1:EzJWgHovont7NscjpAxXsDA8S8BMYve8Y5+7cuRE7R0= -github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1:GwrjFmJcFw6At/Gs6z4yjiIwzuJ1/+UwLxMQDVQXShQ= -github.com/xeipuuv/gojsonschema v1.2.0 h1:LhYJRs+L4fBtjZUfuSZIKGeVu0QRy8e5Xi7D17UxZ74= -github.com/xeipuuv/gojsonschema v1.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQluxsYJ78Id3Y= -github.com/xlab/treeprint v1.2.0 h1:HzHnuAF1plUN2zGlAFHbSQP2qJ0ZAD3XF5XD7OesXRQ= -github.com/xlab/treeprint v1.2.0/go.mod h1:gj5Gd3gPdKtR1ikdDK6fnFLdmIS0X30kTTuNd/WEJu0= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= -github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0= -github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0= go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= -go.opentelemetry.io/contrib/bridges/prometheus v0.57.0 h1:UW0+QyeyBVhn+COBec3nGhfnFe5lwB0ic1JBVjzhk0w= -go.opentelemetry.io/contrib/bridges/prometheus v0.57.0/go.mod h1:ppciCHRLsyCio54qbzQv0E4Jyth/fLWDTJYfvWpcSVk= -go.opentelemetry.io/contrib/exporters/autoexport v0.57.0 h1:jmTVJ86dP60C01K3slFQa2NQ/Aoi7zA+wy7vMOKD9H4= -go.opentelemetry.io/contrib/exporters/autoexport v0.57.0/go.mod h1:EJBheUMttD/lABFyLXhce47Wr6DPWYReCzaZiXadH7g= go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0 h1:F7Jx+6hwnZ41NSFTO5q4LYDtJRXBf2PD0rNBkeB/lus= go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0/go.mod h1:UHB22Z8QsdRDrnAtX4PntOl36ajSxcdUMt1sF7Y6E7Q= go.opentelemetry.io/otel v1.36.0 h1:UumtzIklRBY6cI/lllNZlALOF5nNIzJVb16APdvgTXg= go.opentelemetry.io/otel v1.36.0/go.mod h1:/TcFMXYjyRNh8khOAO9ybYkqaDBb/70aVwkNML4pP8E= -go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.8.0 h1:WzNab7hOOLzdDF/EoWCt4glhrbMPVMOO5JYTmpz36Ls= -go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.8.0/go.mod h1:hKvJwTzJdp90Vh7p6q/9PAOd55dI6WA6sWj62a/JvSs= -go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.8.0 h1:S+LdBGiQXtJdowoJoQPEtI52syEP/JYBUpjO49EQhV8= -go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.8.0/go.mod h1:5KXybFvPGds3QinJWQT7pmXf+TN5YIa7CNYObWRkj50= -go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.32.0 h1:j7ZSD+5yn+lo3sGV69nW04rRR0jhYnBwjuX3r0HvnK0= -go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.32.0/go.mod h1:WXbYJTUaZXAbYd8lbgGuvih0yuCfOFC5RJoYnoLcGz8= -go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.32.0 h1:t/Qur3vKSkUCcDVaSumWF2PKHt85pc7fRvFuoVT8qFU= -go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.32.0/go.mod h1:Rl61tySSdcOJWoEgYZVtmnKdA0GeKrSqkHC1t+91CH8= go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.36.0 h1:dNzwXjZKpMpE2JhmO+9HsPl42NIXFIFSUSSs0fiqra0= go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.36.0/go.mod h1:90PoxvaEB5n6AOdZvi+yWJQoE95U8Dhhw2bSyRqnTD0= -go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.34.0 h1:tgJ0uaNS4c98WRNUEx5U3aDlrDOI5Rs+1Vifcw4DJ8U= -go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.34.0/go.mod h1:U7HYyW0zt/a9x5J1Kjs+r1f/d4ZHnYFclhYY2+YbeoE= go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.34.0 h1:BEj3SPM81McUZHYjRS5pEgNgnmzGJ5tRpU5krWnV8Bs= go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.34.0/go.mod h1:9cKLGBDzI/F3NoHLQGm4ZrYdIHsvGt6ej6hUowxY0J4= -go.opentelemetry.io/otel/exporters/prometheus v0.54.0 h1:rFwzp68QMgtzu9PgP3jm9XaMICI6TsofWWPcBDKwlsU= -go.opentelemetry.io/otel/exporters/prometheus v0.54.0/go.mod h1:QyjcV9qDP6VeK5qPyKETvNjmaaEc7+gqjh4SS0ZYzDU= -go.opentelemetry.io/otel/exporters/stdout/stdoutlog v0.8.0 h1:CHXNXwfKWfzS65yrlB2PVds1IBZcdsX8Vepy9of0iRU= -go.opentelemetry.io/otel/exporters/stdout/stdoutlog v0.8.0/go.mod h1:zKU4zUgKiaRxrdovSS2amdM5gOc59slmo/zJwGX+YBg= -go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.32.0 h1:SZmDnHcgp3zwlPBS2JX2urGYe/jBKEIT6ZedHRUyCz8= -go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.32.0/go.mod h1:fdWW0HtZJ7+jNpTKUR0GpMEDP69nR8YBJQxNiVCE3jk= -go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.32.0 h1:cC2yDI3IQd0Udsux7Qmq8ToKAx1XCilTQECZ0KDZyTw= -go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.32.0/go.mod h1:2PD5Ex6z8CFzDbTdOlwyNIUywRr1DN0ospafJM1wJ+s= -go.opentelemetry.io/otel/log v0.8.0 h1:egZ8vV5atrUWUbnSsHn6vB8R21G2wrKqNiDt3iWertk= -go.opentelemetry.io/otel/log v0.8.0/go.mod h1:M9qvDdUTRCopJcGRKg57+JSQ9LgLBrwwfC32epk5NX8= go.opentelemetry.io/otel/metric v1.36.0 h1:MoWPKVhQvJ+eeXWHFBOPoBOi20jh6Iq2CcCREuTYufE= go.opentelemetry.io/otel/metric v1.36.0/go.mod h1:zC7Ks+yeyJt4xig9DEw9kuUFe5C3zLbVjV2PzT6qzbs= go.opentelemetry.io/otel/sdk v1.36.0 h1:b6SYIuLRs88ztox4EyrvRti80uXIFy+Sqzoh9kFULbs= go.opentelemetry.io/otel/sdk v1.36.0/go.mod h1:+lC+mTgD+MUWfjJubi2vvXWcVxyr9rmlshZni72pXeY= -go.opentelemetry.io/otel/sdk/log v0.8.0 h1:zg7GUYXqxk1jnGF/dTdLPrK06xJdrXgqgFLnI4Crxvs= -go.opentelemetry.io/otel/sdk/log v0.8.0/go.mod h1:50iXr0UVwQrYS45KbruFrEt4LvAdCaWWgIrsN3ZQggo= go.opentelemetry.io/otel/sdk/metric v1.36.0 h1:r0ntwwGosWGaa0CrSt8cuNuTcccMXERFwHX4dThiPis= go.opentelemetry.io/otel/sdk/metric v1.36.0/go.mod h1:qTNOhFDfKRwX0yXOqJYegL5WRaW376QbB7P4Pb0qva4= go.opentelemetry.io/otel/trace v1.36.0 h1:ahxWNuqZjpdiFAyrIoQ4GIiAIhxAunQR6MUoKrsNd4w= go.opentelemetry.io/otel/trace v1.36.0/go.mod h1:gQ+OnDZzrybY4k4seLzPAWNwVBBVlF2szhehOBB/tGA= go.opentelemetry.io/proto/otlp v1.7.0 h1:jX1VolD6nHuFzOYso2E73H85i92Mv8JQYk0K9vz09os= go.opentelemetry.io/proto/otlp v1.7.0/go.mod h1:fSKjH6YJ7HDlwzltzyMj036AJ3ejJLCgCSHGj4efDDo= -go.uber.org/automaxprocs v1.6.0 h1:O3y2/QNTOdbF+e/dpXNNW7Rx2hZ4sTIPyybbxyNqTUs= -go.uber.org/automaxprocs v1.6.0/go.mod h1:ifeIMSnPZuznNm6jmdzmU3/bfk01Fe2fotchwEFJ8r8= -go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= -go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= -go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= -go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= -go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8= -go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= -golang.org/x/crypto v0.0.0-20200302210943-78000ba7a073/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.45.0 h1:jMBrvKuj23MTlT0bQEOBcAE0mjg8mK9RXFhRH6nyF3Q= golang.org/x/crypto v0.45.0/go.mod h1:XTGrrkGJve7CYK7J8PEww4aY7gM3qMCElcJQ8n8JdX4= @@ -468,38 +120,22 @@ golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 h1:2dVuKD2vS7b0QIHQbpyTISPd0 golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56/go.mod h1:M4RDyNAINzryxdtnbRXRL/OHtkFuWGRjvuhBJpk2IlY= golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= -golang.org/x/mod v0.29.0 h1:HV8lRxZC4l2cr3Zq1LvtOsi/ThTgWnUk/y64QSs8GwA= -golang.org/x/mod v0.29.0/go.mod h1:NyhrlYXJ2H4eJiRy/WDBO6HMqZQ6q9nk4JzS3NuCK+w= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.47.0 h1:Mx+4dIFzqraBXUugkia1OOvlD6LemFo1ALMHjrXDOhY= golang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU= -golang.org/x/oauth2 v0.30.0 h1:dnDm7JmhM45NNpd8FDDeLhK6FwqbOf4MLCM9zb1BOHI= -golang.org/x/oauth2 v0.30.0/go.mod h1:B++QgG3ZKulg6sRPGD/mqlHQs5rB3Ml9erfeDY7xKlU= -golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.18.0 h1:kr88TuHDroi+UVf+0hZnirlk8o8T+4MrK6mr60WkH/I= -golang.org/x/sync v0.18.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20201204225414-ed752295db88/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc= golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= -golang.org/x/term v0.37.0 h1:8EGAD0qCmHYZg6J17DvsMy9/wJ7/D/4pV/wfnld5lTU= -golang.org/x/term v0.37.0/go.mod h1:5pB4lxRNYYVZuTLmy8oR2BH8dflOR+IbTYFD8fi3254= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.31.0 h1:aC8ghyu4JhP8VojJ2lEHBnochRno1sgL6nEi9WGFGMM= @@ -511,15 +147,10 @@ golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtn golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0= -golang.org/x/tools v0.38.0 h1:Hx2Xv8hISq8Lm16jvBZ2VQf+RLmbd7wVUsALibYI/IQ= -golang.org/x/tools v0.38.0/go.mod h1:yEsQ/d/YK8cjh0L6rZlY8tgtlKiBNTL14pGDJPJpYQs= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -gomodules.xyz/jsonpatch/v2 v2.4.0 h1:Ci3iUJyx9UeRx7CeFN8ARgGbkESwJK+KB9lLcWxY/Zw= -gomodules.xyz/jsonpatch/v2 v2.4.0/go.mod h1:AH3dM2RI6uoBZxn3LVrfvJ3E0/9dG4cSrbuBJT4moAY= -google.golang.org/genproto v0.0.0-20240123012728-ef4313101c80 h1:KAeGQVN3M9nD0/bQXnr/ClcEMJ968gUXJQ9pwfSynuQ= google.golang.org/genproto/googleapis/api v0.0.0-20250528174236-200df99c418a h1:SGktgSolFCo75dnHJF2yMvnns6jCmHFJ0vE4Vn2JKvQ= google.golang.org/genproto/googleapis/api v0.0.0-20250528174236-200df99c418a/go.mod h1:a77HrdMjoeKbnd2jmgcWdaS++ZLZAEq3orIOAEIKiVw= google.golang.org/genproto/googleapis/rpc v0.0.0-20250528174236-200df99c418a h1:v2PbRU4K3llS09c7zodFpNePeamkAwG3mPrAery9VeE= @@ -529,14 +160,6 @@ google.golang.org/grpc v1.72.2/go.mod h1:wH5Aktxcg25y1I3w7H69nHfXdOG3UiadoBtjh3i google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY= google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= -gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= -gopkg.in/evanphx/json-patch.v4 v4.12.0 h1:n6jtcsulIzXPJaxegRbvFNNrZDjbij7ny3gmSPG+6V4= -gopkg.in/evanphx/json-patch.v4 v4.12.0/go.mod h1:p8EYWUEYMpynmqDbY58zCKCFZw8pRWMG4EsWvDvM72M= -gopkg.in/h2non/gock.v1 v1.1.2 h1:jBbHXgGBK/AoPVfJh5x4r/WxIrElvbLel8TCZkkZJoY= -gopkg.in/h2non/gock.v1 v1.1.2/go.mod h1:n7UGz/ckNChHiK05rDoiC4MYSunEC/lyaUm2WWaDva0= -gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc= -gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= @@ -544,44 +167,3 @@ gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gotest.tools/v3 v3.4.0 h1:ZazjZUfuVeZGLAmlKKuyv3IKP5orXcwtOwDQH6YVr6o= gotest.tools/v3 v3.4.0/go.mod h1:CtbdzLSsqVhDgMtKsx03ird5YTGB3ar27v0u/yKBW5g= -helm.sh/helm/v3 v3.18.4 h1:pNhnHM3nAmDrxz6/UC+hfjDY4yeDATQCka2/87hkZXQ= -helm.sh/helm/v3 v3.18.4/go.mod h1:WVnwKARAw01iEdjpEkP7Ii1tT1pTPYfM1HsakFKM3LI= -k8s.io/api v0.33.2 h1:YgwIS5jKfA+BZg//OQhkJNIfie/kmRsO0BmNaVSimvY= -k8s.io/api v0.33.2/go.mod h1:fhrbphQJSM2cXzCWgqU29xLDuks4mu7ti9vveEnpSXs= -k8s.io/apiextensions-apiserver v0.33.2 h1:6gnkIbngnaUflR3XwE1mCefN3YS8yTD631JXQhsU6M8= -k8s.io/apiextensions-apiserver v0.33.2/go.mod h1:IvVanieYsEHJImTKXGP6XCOjTwv2LUMos0YWc9O+QP8= -k8s.io/apimachinery v0.33.2 h1:IHFVhqg59mb8PJWTLi8m1mAoepkUNYmptHsV+Z1m5jY= -k8s.io/apimachinery v0.33.2/go.mod h1:BHW0YOu7n22fFv/JkYOEfkUYNRN0fj0BlvMFWA7b+SM= -k8s.io/apiserver v0.33.2 h1:KGTRbxn2wJagJowo29kKBp4TchpO1DRO3g+dB/KOJN4= -k8s.io/apiserver v0.33.2/go.mod h1:9qday04wEAMLPWWo9AwqCZSiIn3OYSZacDyu/AcoM/M= -k8s.io/cli-runtime v0.33.2 h1:koNYQKSDdq5AExa/RDudXMhhtFasEg48KLS2KSAU74Y= -k8s.io/cli-runtime v0.33.2/go.mod h1:gnhsAWpovqf1Zj5YRRBBU7PFsRc6NkEkwYNQE+mXL88= -k8s.io/client-go v0.33.2 h1:z8CIcc0P581x/J1ZYf4CNzRKxRvQAwoAolYPbtQes+E= -k8s.io/client-go v0.33.2/go.mod h1:9mCgT4wROvL948w6f6ArJNb7yQd7QsvqavDeZHvNmHo= -k8s.io/component-base v0.33.2 h1:sCCsn9s/dG3ZrQTX/Us0/Sx2R0G5kwa0wbZFYoVp/+0= -k8s.io/component-base v0.33.2/go.mod h1:/41uw9wKzuelhN+u+/C59ixxf4tYQKW7p32ddkYNe2k= -k8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk= -k8s.io/klog/v2 v2.130.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE= -k8s.io/kube-openapi v0.0.0-20250318190949-c8a335a9a2ff h1:/usPimJzUKKu+m+TE36gUyGcf03XZEP0ZIKgKj35LS4= -k8s.io/kube-openapi v0.0.0-20250318190949-c8a335a9a2ff/go.mod h1:5jIi+8yX4RIb8wk3XwBo5Pq2ccx4FP10ohkbSKCZoK8= -k8s.io/kubectl v0.33.2 h1:7XKZ6DYCklu5MZQzJe+CkCjoGZwD1wWl7t/FxzhMz7Y= -k8s.io/kubectl v0.33.2/go.mod h1:8rC67FB8tVTYraovAGNi/idWIK90z2CHFNMmGJZJ3KI= -k8s.io/utils v0.0.0-20250502105355-0f33e8f1c979 h1:jgJW5IePPXLGB8e/1wvd0Ich9QE97RvvF3a8J3fP/Lg= -k8s.io/utils v0.0.0-20250502105355-0f33e8f1c979/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= -oras.land/oras-go/v2 v2.6.0 h1:X4ELRsiGkrbeox69+9tzTu492FMUu7zJQW6eJU+I2oc= -oras.land/oras-go/v2 v2.6.0/go.mod h1:magiQDfG6H1O9APp+rOsvCPcW1GD2MM7vgnKY0Y+u1o= -sigs.k8s.io/controller-runtime v0.21.0 h1:CYfjpEuicjUecRk+KAeyYh+ouUBn4llGyDYytIGcJS8= -sigs.k8s.io/controller-runtime v0.21.0/go.mod h1:OSg14+F65eWqIu4DceX7k/+QRAbTTvxeQSNSOQpukWM= -sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8 h1:gBQPwqORJ8d8/YNZWEjoZs7npUVDpVXUUOFfW6CgAqE= -sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8/go.mod h1:mdzfpAEoE6DHQEN0uh9ZbOCuHbLK5wOm7dK4ctXE9Tg= -sigs.k8s.io/kustomize/api v0.19.0 h1:F+2HB2mU1MSiR9Hp1NEgoU2q9ItNOaBJl0I4Dlus5SQ= -sigs.k8s.io/kustomize/api v0.19.0/go.mod h1:/BbwnivGVcBh1r+8m3tH1VNxJmHSk1PzP5fkP6lbL1o= -sigs.k8s.io/kustomize/kyaml v0.19.0 h1:RFge5qsO1uHhwJsu3ipV7RNolC7Uozc0jUBC/61XSlA= -sigs.k8s.io/kustomize/kyaml v0.19.0/go.mod h1:FeKD5jEOH+FbZPpqUghBP8mrLjJ3+zD3/rf9NNu1cwY= -sigs.k8s.io/randfill v0.0.0-20250304075658-069ef1bbf016/go.mod h1:XeLlZ/jmk4i1HRopwe7/aU3H5n1zNUcX6TM94b3QxOY= -sigs.k8s.io/randfill v1.0.0 h1:JfjMILfT8A6RbawdsK2JXGBR5AQVfd+9TbzrlneTyrU= -sigs.k8s.io/randfill v1.0.0/go.mod h1:XeLlZ/jmk4i1HRopwe7/aU3H5n1zNUcX6TM94b3QxOY= -sigs.k8s.io/structured-merge-diff/v4 v4.6.0 h1:IUA9nvMmnKWcj5jl84xn+T5MnlZKThmUW1TdblaLVAc= -sigs.k8s.io/structured-merge-diff/v4 v4.6.0/go.mod h1:dDy58f92j70zLsuZVuUX5Wp9vtxXpaZnkPGWeqDfCps= -sigs.k8s.io/yaml v1.4.0 h1:Mk1wCc2gy/F0THH0TAp1QYyJNzRm2KCLy3o5ASXVI5E= -sigs.k8s.io/yaml v1.4.0/go.mod h1:Ejl7/uTz7PSA4eKMyQCUTnhZYNmLIl+5c2lQPGR2BPY= From 7a765c4819988b23e7fdf892fc30184813f5937c Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 17 Feb 2026 13:59:16 +0100 Subject: [PATCH 020/121] Bump the minor-and-patches group across 1 directory with 3 updates (#19469) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps the minor-and-patches group with 3 updates in the / directory: [docker/login-action](https://github.com/docker/login-action), [actions/setup-python](https://github.com/actions/setup-python) and [actions/cache](https://github.com/actions/cache). Updates `docker/login-action` from 3.6.0 to 3.7.0
Release notes

Sourced from docker/login-action's releases.

v3.7.0

Full Changelog: https://github.com/docker/login-action/compare/v3.6.0...v3.7.0

Commits
  • c94ce9f Merge pull request #915 from docker/dependabot/npm_and_yarn/lodash-4.17.23
  • 8339c95 Merge pull request #912 from docker/scope
  • c83e932 build(deps): bump lodash from 4.17.21 to 4.17.23
  • b268aa5 chore: update generated content
  • a603229 documentation for scope input
  • 7567f92 Add scope input to set scopes for the authentication token
  • 0567fa5 Merge pull request #914 from dphi/add-support-for-amazonaws.eu
  • f6ef577 feat: add support for AWS European Sovereign Cloud ECR registries
  • 916386b Merge pull request #911 from crazy-max/ensure-redact
  • 5b3f94a chore: update generated content
  • Additional commits viewable in compare view

Updates `actions/setup-python` from 6.1.0 to 6.2.0
Release notes

Sourced from actions/setup-python's releases.

v6.2.0

What's Changed

Dependency Upgrades

Full Changelog: https://github.com/actions/setup-python/compare/v6...v6.2.0

Commits

Updates `actions/cache` from 5.0.1 to 5.0.3
Release notes

Sourced from actions/cache's releases.

v5.0.3

What's Changed

Full Changelog: https://github.com/actions/cache/compare/v5...v5.0.3

v.5.0.2

v5.0.2

What's Changed

When creating cache entries, 429s returned from the cache service will not be retried.

Changelog

Sourced from actions/cache's changelog.

Releases

How to prepare a release

[!NOTE]
Relevant for maintainers with write access only.

  1. Switch to a new branch from main.
  2. Run npm test to ensure all tests are passing.
  3. Update the version in https://github.com/actions/cache/blob/main/package.json.
  4. Run npm run build to update the compiled files.
  5. Update this https://github.com/actions/cache/blob/main/RELEASES.md with the new version and changes in the ## Changelog section.
  6. Run licensed cache to update the license report.
  7. Run licensed status and resolve any warnings by updating the https://github.com/actions/cache/blob/main/.licensed.yml file with the exceptions.
  8. Commit your changes and push your branch upstream.
  9. Open a pull request against main and get it reviewed and merged.
  10. Draft a new release https://github.com/actions/cache/releases use the same version number used in package.json
    1. Create a new tag with the version number.
    2. Auto generate release notes and update them to match the changes you made in RELEASES.md.
    3. Toggle the set as the latest release option.
    4. Publish the release.
  11. Navigate to https://github.com/actions/cache/actions/workflows/release-new-action-version.yml
    1. There should be a workflow run queued with the same version number.
    2. Approve the run to publish the new version and update the major tags for this action.

Changelog

5.0.3

5.0.2

  • Bump @actions/cache to v5.0.3 #1692

5.0.1

  • Update @azure/storage-blob to ^12.29.1 via @actions/cache@5.0.1 #1685

5.0.0

[!IMPORTANT] actions/cache@v5 runs on the Node.js 24 runtime and requires a minimum Actions Runner version of 2.327.1. If you are using self-hosted runners, ensure they are updated before upgrading.

4.3.0

  • Bump @actions/cache to v4.1.0

... (truncated)

Commits

Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`. [//]: # (dependabot-automerge-start) [//]: # (dependabot-automerge-end) ---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot show ignore conditions` will show all of the ignore conditions of the specified dependency - `@dependabot ignore major version` will close this group update PR and stop Dependabot creating any more for the specific dependency's major version (unless you unignore this specific dependency's major version or upgrade to it yourself) - `@dependabot ignore minor version` will close this group update PR and stop Dependabot creating any more for the specific dependency's minor version (unless you unignore this specific dependency's minor version or upgrade to it yourself) - `@dependabot ignore ` will close this group update PR and stop Dependabot creating any more for the specific dependency (unless you unignore this specific dependency or upgrade to it yourself) - `@dependabot unignore ` will remove all of the ignore conditions of the specified dependency - `@dependabot unignore ` will remove the ignore condition of the specified dependency and ignore conditions
Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/docker.yml | 8 ++++---- .github/workflows/docs-pr.yaml | 2 +- .github/workflows/docs.yaml | 2 +- .github/workflows/latest_deps.yml | 2 +- .github/workflows/poetry_lockfile.yaml | 2 +- .github/workflows/push_complement_image.yml | 2 +- .github/workflows/release-artifacts.yml | 10 +++++----- .github/workflows/schema.yaml | 4 ++-- .github/workflows/tests.yml | 14 +++++++------- 9 files changed, 23 insertions(+), 23 deletions(-) diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml index bcd65b2f600..80504c569c1 100644 --- a/.github/workflows/docker.yml +++ b/.github/workflows/docker.yml @@ -41,13 +41,13 @@ jobs: echo "SYNAPSE_VERSION=$(grep "^version" pyproject.toml | sed -E 's/version\s*=\s*["]([^"]*)["]/\1/')" >> $GITHUB_ENV - name: Log in to DockerHub - uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3.6.0 + uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3.7.0 with: username: ${{ secrets.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKERHUB_TOKEN }} - name: Log in to GHCR - uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3.6.0 + uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3.7.0 with: registry: ghcr.io username: ${{ github.repository_owner }} @@ -102,14 +102,14 @@ jobs: merge-multiple: true - name: Log in to DockerHub - uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3.6.0 + uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3.7.0 if: ${{ startsWith(matrix.repository, 'docker.io') }} with: username: ${{ secrets.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKERHUB_TOKEN }} - name: Log in to GHCR - uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3.6.0 + uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3.7.0 if: ${{ startsWith(matrix.repository, 'ghcr.io') }} with: registry: ghcr.io diff --git a/.github/workflows/docs-pr.yaml b/.github/workflows/docs-pr.yaml index 424857822b1..41f3664336f 100644 --- a/.github/workflows/docs-pr.yaml +++ b/.github/workflows/docs-pr.yaml @@ -24,7 +24,7 @@ jobs: mdbook-version: '0.5.2' - name: Setup python - uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 # v6.1.0 + uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 with: python-version: "3.x" diff --git a/.github/workflows/docs.yaml b/.github/workflows/docs.yaml index 11fd8c6d6ca..31f3b05b8e6 100644 --- a/.github/workflows/docs.yaml +++ b/.github/workflows/docs.yaml @@ -64,7 +64,7 @@ jobs: run: echo 'window.SYNAPSE_VERSION = "${{ needs.pre.outputs.branch-version }}";' > ./docs/website_files/version.js - name: Setup python - uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 # v6.1.0 + uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 with: python-version: "3.x" diff --git a/.github/workflows/latest_deps.yml b/.github/workflows/latest_deps.yml index dda4e5f57bf..5bc78062cda 100644 --- a/.github/workflows/latest_deps.yml +++ b/.github/workflows/latest_deps.yml @@ -93,7 +93,7 @@ jobs: -e POSTGRES_PASSWORD=postgres \ -e POSTGRES_INITDB_ARGS="--lc-collate C --lc-ctype C --encoding UTF8" \ postgres:${{ matrix.postgres-version }} - - uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 # v6.1.0 + - uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 with: python-version: "3.x" - run: pip install .[all,test] diff --git a/.github/workflows/poetry_lockfile.yaml b/.github/workflows/poetry_lockfile.yaml index fb4c449b58d..5042a564f2a 100644 --- a/.github/workflows/poetry_lockfile.yaml +++ b/.github/workflows/poetry_lockfile.yaml @@ -17,7 +17,7 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - - uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 # v6.1.0 + - uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 with: python-version: '3.x' - run: pip install tomli diff --git a/.github/workflows/push_complement_image.yml b/.github/workflows/push_complement_image.yml index e6d0894e838..12b4720ca5a 100644 --- a/.github/workflows/push_complement_image.yml +++ b/.github/workflows/push_complement_image.yml @@ -48,7 +48,7 @@ jobs: with: ref: master - name: Login to registry - uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3.6.0 + uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3.7.0 with: registry: ghcr.io username: ${{ github.actor }} diff --git a/.github/workflows/release-artifacts.yml b/.github/workflows/release-artifacts.yml index 5f5b64dc643..4343cb482f2 100644 --- a/.github/workflows/release-artifacts.yml +++ b/.github/workflows/release-artifacts.yml @@ -28,7 +28,7 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - - uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 # v6.1.0 + - uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 with: python-version: "3.x" - id: set-distros @@ -64,7 +64,7 @@ jobs: uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3.12.0 - name: Set up docker layer caching - uses: actions/cache@9255dc7a253b0ccc959486e2bca901246202afeb # v5.0.1 + uses: actions/cache@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5.0.3 with: path: /tmp/.buildx-cache key: ${{ runner.os }}-buildx-${{ github.sha }} @@ -72,7 +72,7 @@ jobs: ${{ runner.os }}-buildx- - name: Set up python - uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 # v6.1.0 + uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 with: python-version: "3.x" @@ -125,7 +125,7 @@ jobs: steps: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - - uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 # v6.1.0 + - uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 with: # setup-python@v4 doesn't impose a default python version. Need to use 3.x # here, because `python` on osx points to Python 2.7. @@ -162,7 +162,7 @@ jobs: steps: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - - uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 # v6.1.0 + - uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 with: python-version: "3.10" diff --git a/.github/workflows/schema.yaml b/.github/workflows/schema.yaml index b068e976db0..430ff8861aa 100644 --- a/.github/workflows/schema.yaml +++ b/.github/workflows/schema.yaml @@ -15,7 +15,7 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - - uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 # v6.1.0 + - uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 with: python-version: "3.x" - name: Install check-jsonschema @@ -41,7 +41,7 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - - uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 # v6.1.0 + - uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 with: python-version: "3.x" - name: Install PyYAML diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index f575adb5c57..81a72f1f688 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -107,7 +107,7 @@ jobs: steps: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - - uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 # v6.1.0 + - uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 with: python-version: "3.x" - run: "pip install 'click==8.1.1' 'GitPython>=3.1.20' 'sqlglot>=28.0.0'" @@ -117,7 +117,7 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - - uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 # v6.1.0 + - uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 with: python-version: "3.x" - run: .ci/scripts/check_lockfile.py @@ -174,7 +174,7 @@ jobs: # Cribbed from # https://github.com/AustinScola/mypy-cache-github-action/blob/85ea4f2972abed39b33bd02c36e341b28ca59213/src/restore.ts#L10-L17 - name: Restore/persist mypy's cache - uses: actions/cache@9255dc7a253b0ccc959486e2bca901246202afeb # v5.0.1 + uses: actions/cache@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5.0.3 with: path: | .mypy_cache @@ -200,7 +200,7 @@ jobs: with: ref: ${{ github.event.pull_request.head.sha }} fetch-depth: 0 - - uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 # v6.1.0 + - uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 with: python-version: "3.x" - run: "pip install 'towncrier>=18.6.0rc1'" @@ -308,7 +308,7 @@ jobs: if: ${{ needs.changes.outputs.linting_readme == 'true' }} steps: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - - uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 # v6.1.0 + - uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 with: python-version: "3.x" - run: "pip install rstcheck" @@ -355,7 +355,7 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - - uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 # v6.1.0 + - uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 with: python-version: "3.x" - id: get-matrix @@ -446,7 +446,7 @@ jobs: sudo apt-get -qq install build-essential libffi-dev python3-dev \ libxml2-dev libxslt-dev xmlsec1 zlib1g-dev libjpeg-dev libwebp-dev - - uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 # v6.1.0 + - uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 with: python-version: "3.10" From b0b4203cb6090b9c79e597d65eadebb5b3fbb843 Mon Sep 17 00:00:00 2001 From: Eric Eastwood Date: Tue, 17 Feb 2026 07:04:17 -0600 Subject: [PATCH 021/121] Fix `/sync` missing membership in `state_after` (#19463) --- changelog.d/19463.bugfix | 1 + synapse/handlers/sync.py | 15 ++++++++++++--- 2 files changed, 13 insertions(+), 3 deletions(-) create mode 100644 changelog.d/19463.bugfix diff --git a/changelog.d/19463.bugfix b/changelog.d/19463.bugfix new file mode 100644 index 00000000000..fe44c2ddf66 --- /dev/null +++ b/changelog.d/19463.bugfix @@ -0,0 +1 @@ +Fix `/sync` missing membership event in `state_after` (experimental [MSC4222](https://github.com/matrix-org/matrix-spec-proposals/pull/4222) implementation) in some scenarios. diff --git a/synapse/handlers/sync.py b/synapse/handlers/sync.py index 72e91d66ac4..ab050adb262 100644 --- a/synapse/handlers/sync.py +++ b/synapse/handlers/sync.py @@ -1041,9 +1041,18 @@ async def compute_state_delta( if event.sender not in first_event_by_sender_map: first_event_by_sender_map[event.sender] = event - # We need the event's sender, unless their membership was in a - # previous timeline event. - if (EventTypes.Member, event.sender) not in timeline_state: + # When using `state_after`, there is no special treatment with + # regards to state also being in the `timeline`. Always fetch + # relevant membership regardless of whether the state event is in + # the `timeline`. + if sync_config.use_state_after: + members_to_fetch.add(event.sender) + # For `state`, the client is supposed to do a flawed re-construction + # of state over time by starting with the given `state` and layering + # on state from the `timeline` as you go (flawed because state + # resolution). In this case, we only need their membership in + # `state` when their membership isn't already in the `timeline`. + elif (EventTypes.Member, event.sender) not in timeline_state: members_to_fetch.add(event.sender) # FIXME: we also care about invite targets etc. From 69b931f30572a7fefdd2bd548550585245cad2b9 Mon Sep 17 00:00:00 2001 From: Devon Hudson Date: Tue, 17 Feb 2026 13:35:38 +0000 Subject: [PATCH 022/121] Push Synapse docker images to Element OCI Registry (#19420) Part of https://github.com/element-hq/serverproduct-internal/issues/1153 Pushes Synapse docker images to the Element OCI Registry in addition to the dockerhub and ghcr registries. Ready for review despite Draft status. See https://github.com/element-hq/synapse/pull/19420#issuecomment-3848866071 ### Pull Request Checklist * [X] Pull request is based on the develop branch * [X] Pull request includes a [changelog file](https://element-hq.github.io/synapse/latest/development/contributing_guide.html#changelog). The entry should: - Be a short description of your change which makes sense to users. "Fixed a bug that prevented receiving messages from other servers." instead of "Moved X method from `EventStore` to `EventWorkerStore`.". - Use markdown where necessary, mostly for `code blocks`. - End with either a period (.) or an exclamation mark (!). - Start with a capital letter. - Feel free to credit yourself, by adding a sentence "Contributed by @github_username." or "Contributed by [Your Name]." to the end of the entry. * [X] [Code style](https://element-hq.github.io/synapse/latest/code_style.html) is correct (run the [linters](https://element-hq.github.io/synapse/latest/development/contributing_guide.html#run-the-linters)) --------- Co-authored-by: Quentin Gliech --- .github/workflows/docker.yml | 66 ++++++++++++++++++++++++++++++++++++ changelog.d/19420.misc | 1 + 2 files changed, 67 insertions(+) create mode 100644 changelog.d/19420.misc diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml index 80504c569c1..ca20e9652db 100644 --- a/.github/workflows/docker.yml +++ b/.github/workflows/docker.yml @@ -53,6 +53,38 @@ jobs: username: ${{ github.repository_owner }} password: ${{ secrets.GITHUB_TOKEN }} + - name: Tailscale + uses: tailscale/github-action@53acf823325fe9ca47f4cdaa951f90b4b0de5bb9 # v4.1.1 + with: + oauth-client-id: ${{ secrets.TS_OAUTH_CLIENT_ID }} + audience: ${{ secrets.TS_AUDIENCE }} + tags: tag:github-actions + + - name: Compute vault jwt role name + id: vault-jwt-role + run: | + echo "role_name=github_service_management_$( echo "${{ github.repository }}" | sed -r 's|[/-]|_|g')" | tee -a "$GITHUB_OUTPUT" + + - name: Get team registry token + id: import-secrets + uses: hashicorp/vault-action@4c06c5ccf5c0761b6029f56cfb1dcf5565918a3b # v3.4.0 + with: + url: https://vault.infra.ci.i.element.dev + role: ${{ steps.vault-jwt-role.outputs.role_name }} + path: service-management/github-actions + jwtGithubAudience: https://vault.infra.ci.i.element.dev + method: jwt + secrets: | + services/backend-repositories/secret/data/oci.element.io username | OCI_USERNAME ; + services/backend-repositories/secret/data/oci.element.io password | OCI_PASSWORD ; + + - name: Login to Element OCI Registry + uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3.6.0 + with: + registry: oci-push.vpn.infra.element.io + username: ${{ steps.import-secrets.outputs.OCI_USERNAME }} + password: ${{ steps.import-secrets.outputs.OCI_PASSWORD }} + - name: Build and push by digest id: build uses: docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83 # v6.18.0 @@ -64,6 +96,7 @@ jobs: tags: | docker.io/matrixdotorg/synapse ghcr.io/element-hq/synapse + oci-push.vpn.infra.element.io/synapse file: "docker/Dockerfile" platforms: ${{ matrix.platform }} outputs: type=image,push-by-digest=true,name-canonical=true,push=true @@ -90,6 +123,7 @@ jobs: repository: - docker.io/matrixdotorg/synapse - ghcr.io/element-hq/synapse + - oci-push.vpn.infra.element.io/synapse needs: - build @@ -116,6 +150,38 @@ jobs: username: ${{ github.repository_owner }} password: ${{ secrets.GITHUB_TOKEN }} + - name: Tailscale + uses: tailscale/github-action@53acf823325fe9ca47f4cdaa951f90b4b0de5bb9 # v4.1.1 + with: + oauth-client-id: ${{ secrets.TS_OAUTH_CLIENT_ID }} + audience: ${{ secrets.TS_AUDIENCE }} + tags: tag:github-actions + + - name: Compute vault jwt role name + id: vault-jwt-role + run: | + echo "role_name=github_service_management_$( echo "${{ github.repository }}" | sed -r 's|[/-]|_|g')" | tee -a "$GITHUB_OUTPUT" + + - name: Get team registry token + id: import-secrets + uses: hashicorp/vault-action@4c06c5ccf5c0761b6029f56cfb1dcf5565918a3b # v3.4.0 + with: + url: https://vault.infra.ci.i.element.dev + role: ${{ steps.vault-jwt-role.outputs.role_name }} + path: service-management/github-actions + jwtGithubAudience: https://vault.infra.ci.i.element.dev + method: jwt + secrets: | + services/backend-repositories/secret/data/oci.element.io username | OCI_USERNAME ; + services/backend-repositories/secret/data/oci.element.io password | OCI_PASSWORD ; + + - name: Login to Element OCI Registry + uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3.6.0 + with: + registry: oci-push.vpn.infra.element.io + username: ${{ steps.import-secrets.outputs.OCI_USERNAME }} + password: ${{ steps.import-secrets.outputs.OCI_PASSWORD }} + - name: Set up Docker Buildx uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3.12.0 diff --git a/changelog.d/19420.misc b/changelog.d/19420.misc new file mode 100644 index 00000000000..7b3718ce1a1 --- /dev/null +++ b/changelog.d/19420.misc @@ -0,0 +1 @@ +Push Synapse docker images to Element OCI Registry. From 3669d6e3df77db699b60b12cf061db8f5b995f73 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 17 Feb 2026 14:43:41 +0100 Subject: [PATCH 023/121] Bump reqwest from 0.12.26 to 0.12.28 (#19444) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps [reqwest](https://github.com/seanmonstar/reqwest) from 0.12.26 to 0.12.28.
Release notes

Sourced from reqwest's releases.

v0.12.28

What's Changed

Full Changelog: https://github.com/seanmonstar/reqwest/compare/v0.12.27...v0.12.28

v0.12.27

tl;dr

  • Add ClientBuilder::windows_named_pipe(name) option that will force all requests over that Windows Named Pipe.

What's Changed

Full Changelog: https://github.com/seanmonstar/reqwest/compare/v0.12.26...v0.12.27

Changelog

Sourced from reqwest's changelog.

v0.12.28

  • Fix compiling on Windows if TLS and SOCKS features are not enabled.

v0.12.27

  • Add ClientBuilder::windows_named_pipe(name) option that will force all requests over that Windows Named Piper.
Commits

[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=reqwest&package-manager=cargo&previous-version=0.12.26&new-version=0.12.28)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores) Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`. [//]: # (dependabot-automerge-start) [//]: # (dependabot-automerge-end) ---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot show ignore conditions` will show all of the ignore conditions of the specified dependency - `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself)
Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Quentin Gliech --- Cargo.lock | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index c981d139d4a..47f81eaef1d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1024,9 +1024,9 @@ checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" [[package]] name = "reqwest" -version = "0.12.26" +version = "0.12.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b4c14b2d9afca6a60277086b0cc6a6ae0b568f6f7916c943a8cdc79f8be240f" +checksum = "eddd3ca559203180a307f12d114c268abf583f59b03cb906fd0b3ff8646c1147" dependencies = [ "base64", "bytes", From e627b08786cb24c966e8f4f4da92b34035dde9fe Mon Sep 17 00:00:00 2001 From: Erik Johnston Date: Tue, 17 Feb 2026 14:48:59 +0100 Subject: [PATCH 024/121] Add cargo.lock to Rust build hash (#19470) This is so that when we update dependencies etc we correctly ensure that the Rust library has been rebuilt. --- changelog.d/19470.misc | 1 + rust/build.rs | 14 ++++++++++++++ synapse/util/rust.py | 19 +++++++++++++------ 3 files changed, 28 insertions(+), 6 deletions(-) create mode 100644 changelog.d/19470.misc diff --git a/changelog.d/19470.misc b/changelog.d/19470.misc new file mode 100644 index 00000000000..e253d89b17e --- /dev/null +++ b/changelog.d/19470.misc @@ -0,0 +1 @@ +Correctly refuse to start if the Rust workspace config has changed and the Rust library has not been rebuilt. diff --git a/rust/build.rs b/rust/build.rs index ef370e6b418..8755f3bfa38 100644 --- a/rust/build.rs +++ b/rust/build.rs @@ -29,6 +29,13 @@ fn main() -> Result<(), std::io::Error> { } } + // Manually add Cargo.toml's, Cargo.lock and build.rs to the hash, since changes to + // these files should also invalidate the built module. + paths.push("Cargo.toml".to_string()); + paths.push("../Cargo.lock".to_string()); + paths.push("../Cargo.toml".to_string()); + paths.push("build.rs".to_string()); + paths.sort(); let mut hasher = Blake2b512::new(); @@ -41,5 +48,12 @@ fn main() -> Result<(), std::io::Error> { let hex_digest = hex::encode(hasher.finalize()); println!("cargo:rustc-env=SYNAPSE_RUST_DIGEST={hex_digest}"); + // The default rules don't pick up trivial changes to the workspace config + // files, but we need to rebuild if those change to pick up the changed + // hashes. + println!("cargo::rerun-if-changed=."); + println!("cargo::rerun-if-changed=../Cargo.lock"); + println!("cargo::rerun-if-changed=../Cargo.toml"); + Ok(()) } diff --git a/synapse/util/rust.py b/synapse/util/rust.py index d1e1a259e42..a7c9e976ea4 100644 --- a/synapse/util/rust.py +++ b/synapse/util/rust.py @@ -40,24 +40,24 @@ def check_rust_lib_up_to_date() -> None: return None # Get the hash of all Rust source files - rust_path = os.path.join(synapse_root, "rust", "src") + rust_path = os.path.join(synapse_root, "rust") if not os.path.exists(rust_path): return None - hash = _hash_rust_files_in_directory(rust_path) + hash = _hash_rust_files_in_directory(synapse_root) if hash != get_rust_file_digest(): raise Exception("Rust module outdated. Please rebuild using `poetry install`") -def _hash_rust_files_in_directory(directory: str) -> str: +def _hash_rust_files_in_directory(synapse_root: str) -> str: """Get the hash of all files in a directory (recursively)""" - directory = os.path.abspath(directory) + src_directory = os.path.abspath(os.path.join(synapse_root, "rust", "src")) paths = [] - dirs = [directory] + dirs = [src_directory] while dirs: dir = dirs.pop() with os.scandir(dir) as d: @@ -67,13 +67,20 @@ def _hash_rust_files_in_directory(directory: str) -> str: else: paths.append(entry.path) + # Manually add Cargo.toml's, Cargo.lock and build.rs to the hash, since + # changes to these files should also invalidate the built module. + paths.append(os.path.join(synapse_root, "rust", "Cargo.toml")) + paths.append(os.path.join(synapse_root, "rust", "build.rs")) + paths.append(os.path.join(synapse_root, "Cargo.lock")) + paths.append(os.path.join(synapse_root, "Cargo.toml")) + # We sort to make sure that we get a consistent and well-defined ordering. paths.sort() hasher = blake2b() for path in paths: - with open(os.path.join(directory, path), "rb") as f: + with open(path, "rb") as f: hasher.update(f.read()) return hasher.hexdigest() From bd0756c6ca6edd83149bfaf30a874d09bd9cbf0b Mon Sep 17 00:00:00 2001 From: Quentin Gliech Date: Tue, 17 Feb 2026 17:41:14 +0100 Subject: [PATCH 025/121] Revert "Fix `/sync` missing membership in `state_after`" (#19474) Reverts element-hq/synapse#19463 The complement tests haven't been reviewed and require more testing. Discussed in the internal [backend team lobby](https://matrix.to/#/!SGNQGPGUwtcPBUotTL:matrix.org/$XDARK2u2iLL5wWaxiL6tJYkLg80Sn6yWWEQib8ahl5Q?via=jki.re&via=element.io&via=matrix.org) room. --- changelog.d/19463.bugfix | 1 - synapse/handlers/sync.py | 15 +++------------ 2 files changed, 3 insertions(+), 13 deletions(-) delete mode 100644 changelog.d/19463.bugfix diff --git a/changelog.d/19463.bugfix b/changelog.d/19463.bugfix deleted file mode 100644 index fe44c2ddf66..00000000000 --- a/changelog.d/19463.bugfix +++ /dev/null @@ -1 +0,0 @@ -Fix `/sync` missing membership event in `state_after` (experimental [MSC4222](https://github.com/matrix-org/matrix-spec-proposals/pull/4222) implementation) in some scenarios. diff --git a/synapse/handlers/sync.py b/synapse/handlers/sync.py index ab050adb262..72e91d66ac4 100644 --- a/synapse/handlers/sync.py +++ b/synapse/handlers/sync.py @@ -1041,18 +1041,9 @@ async def compute_state_delta( if event.sender not in first_event_by_sender_map: first_event_by_sender_map[event.sender] = event - # When using `state_after`, there is no special treatment with - # regards to state also being in the `timeline`. Always fetch - # relevant membership regardless of whether the state event is in - # the `timeline`. - if sync_config.use_state_after: - members_to_fetch.add(event.sender) - # For `state`, the client is supposed to do a flawed re-construction - # of state over time by starting with the given `state` and layering - # on state from the `timeline` as you go (flawed because state - # resolution). In this case, we only need their membership in - # `state` when their membership isn't already in the `timeline`. - elif (EventTypes.Member, event.sender) not in timeline_state: + # We need the event's sender, unless their membership was in a + # previous timeline event. + if (EventTypes.Member, event.sender) not in timeline_state: members_to_fetch.add(event.sender) # FIXME: we also care about invite targets etc. From 40171d036acec6f0315bbac77bd7f073bd64feb9 Mon Sep 17 00:00:00 2001 From: Quentin Gliech Date: Tue, 17 Feb 2026 17:44:37 +0100 Subject: [PATCH 026/121] 1.148.0rc1 --- CHANGES.md | 24 ++++++++++++++++++++++++ changelog.d/19365.feature | 1 - changelog.d/19406.misc | 1 - changelog.d/19420.misc | 1 - changelog.d/19429.removal | 1 - changelog.d/19435.doc | 1 - changelog.d/19457.misc | 1 - changelog.d/19470.misc | 1 - debian/changelog | 6 ++++++ pyproject.toml | 2 +- schema/synapse-config.schema.yaml | 2 +- 11 files changed, 32 insertions(+), 9 deletions(-) delete mode 100644 changelog.d/19365.feature delete mode 100644 changelog.d/19406.misc delete mode 100644 changelog.d/19420.misc delete mode 100644 changelog.d/19429.removal delete mode 100644 changelog.d/19435.doc delete mode 100644 changelog.d/19457.misc delete mode 100644 changelog.d/19470.misc diff --git a/CHANGES.md b/CHANGES.md index fe27ccb040a..e4bec3aa41c 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,3 +1,27 @@ +# Synapse 1.148.0rc1 (2026-02-17) + +## Features + +- Support sending and receiving [MSC4354 Sticky Event](https://github.com/matrix-org/matrix-spec-proposals/pull/4354) metadata. ([\#19365](https://github.com/element-hq/synapse/issues/19365)) + +## Improved Documentation + +- Fix reference to the `experimental_features` section of config. ([\#19435](https://github.com/element-hq/synapse/issues/19435)) + +## Deprecations and Removals + +- Remove support for [MSC3244: Room version capabilities](https://github.com/matrix-org/matrix-spec-proposals/pull/3244) as the MSC was rejected. ([\#19429](https://github.com/element-hq/synapse/issues/19429)) + +## Internal Changes + +- Add in-repo Complement tests so we can test Synapse specific behavior at an end-to-end level. ([\#19406](https://github.com/element-hq/synapse/issues/19406)) +- Push Synapse docker images to Element OCI Registry. ([\#19420](https://github.com/element-hq/synapse/issues/19420)) +- Allow configuring the Rust HTTP client to use HTTP/2 only. ([\#19457](https://github.com/element-hq/synapse/issues/19457)) +- Correctly refuse to start if the Rust workspace config has changed and the Rust library has not been rebuilt. ([\#19470](https://github.com/element-hq/synapse/issues/19470)) + + + + # Synapse 1.147.1 (2026-02-12) ## Internal Changes diff --git a/changelog.d/19365.feature b/changelog.d/19365.feature deleted file mode 100644 index c35afdc179e..00000000000 --- a/changelog.d/19365.feature +++ /dev/null @@ -1 +0,0 @@ -Support sending and receiving [MSC4354 Sticky Event](https://github.com/matrix-org/matrix-spec-proposals/pull/4354) metadata. \ No newline at end of file diff --git a/changelog.d/19406.misc b/changelog.d/19406.misc deleted file mode 100644 index bfe8d6afb0f..00000000000 --- a/changelog.d/19406.misc +++ /dev/null @@ -1 +0,0 @@ -Add in-repo Complement tests so we can test Synapse specific behavior at an end-to-end level. diff --git a/changelog.d/19420.misc b/changelog.d/19420.misc deleted file mode 100644 index 7b3718ce1a1..00000000000 --- a/changelog.d/19420.misc +++ /dev/null @@ -1 +0,0 @@ -Push Synapse docker images to Element OCI Registry. diff --git a/changelog.d/19429.removal b/changelog.d/19429.removal deleted file mode 100644 index b52ca2423d4..00000000000 --- a/changelog.d/19429.removal +++ /dev/null @@ -1 +0,0 @@ -Remove support for [MSC3244: Room version capabilities](https://github.com/matrix-org/matrix-spec-proposals/pull/3244) as the MSC was rejected. \ No newline at end of file diff --git a/changelog.d/19435.doc b/changelog.d/19435.doc deleted file mode 100644 index 4edf2f73dce..00000000000 --- a/changelog.d/19435.doc +++ /dev/null @@ -1 +0,0 @@ -Fix reference to the `experimental_features` section of config. diff --git a/changelog.d/19457.misc b/changelog.d/19457.misc deleted file mode 100644 index ccbb7f5b144..00000000000 --- a/changelog.d/19457.misc +++ /dev/null @@ -1 +0,0 @@ -Allow configuring the Rust HTTP client to use HTTP/2 only. diff --git a/changelog.d/19470.misc b/changelog.d/19470.misc deleted file mode 100644 index e253d89b17e..00000000000 --- a/changelog.d/19470.misc +++ /dev/null @@ -1 +0,0 @@ -Correctly refuse to start if the Rust workspace config has changed and the Rust library has not been rebuilt. diff --git a/debian/changelog b/debian/changelog index a6852dac5e3..105ea2fcec1 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,3 +1,9 @@ +matrix-synapse-py3 (1.148.0~rc1) stable; urgency=medium + + * New synapse release 1.148.0rc1. + + -- Synapse Packaging team Tue, 17 Feb 2026 16:44:08 +0000 + matrix-synapse-py3 (1.147.1) stable; urgency=medium * New synapse release 1.147.1. diff --git a/pyproject.toml b/pyproject.toml index 8073f8ec44f..07455c57df5 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "matrix-synapse" -version = "1.147.1" +version = "1.148.0rc1" description = "Homeserver for the Matrix decentralised comms protocol" readme = "README.rst" authors = [ diff --git a/schema/synapse-config.schema.yaml b/schema/synapse-config.schema.yaml index 99c9b6e9bd1..42e9ed87dda 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.147/synapse-config.schema.json +$id: https://element-hq.github.io/synapse/schema/synapse/v1.148/synapse-config.schema.json type: object properties: modules: From 32c5f01bcb6e7070bfb014569e9979de3df26959 Mon Sep 17 00:00:00 2001 From: Quentin Gliech Date: Tue, 17 Feb 2026 18:33:08 +0100 Subject: [PATCH 027/121] Minor reword --- CHANGES.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGES.md b/CHANGES.md index e4bec3aa41c..e52ca3ed30f 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -6,7 +6,7 @@ ## Improved Documentation -- Fix reference to the `experimental_features` section of config. ([\#19435](https://github.com/element-hq/synapse/issues/19435)) +- Fix reference to the `experimental_features` section of the configuration manual documentation. ([\#19435](https://github.com/element-hq/synapse/issues/19435)) ## Deprecations and Removals From b80774efb2ea5ae780ab751aad5de52ac7a2b0de Mon Sep 17 00:00:00 2001 From: Eric Eastwood Date: Tue, 17 Feb 2026 13:15:57 -0600 Subject: [PATCH 028/121] Better instrument `JoinRoomAliasServlet` with tracing (#19461) So we can better see why it decides to do a local vs remote join. Spawning from [investigating a join issue on `matrix.org`](https://matrix.to/#/!SGNQGPGUwtcPBUotTL:matrix.org/$Odvd47QtkRscxilzkhcFOsDZWNvJUSEhSrD8GpukKWo?via=jki.re&via=element.io&via=matrix.org). --- changelog.d/19461.misc | 1 + synapse/handlers/room_member.py | 24 +++++++++++++++++++++--- synapse/handlers/room_member_worker.py | 5 +++++ 3 files changed, 27 insertions(+), 3 deletions(-) create mode 100644 changelog.d/19461.misc diff --git a/changelog.d/19461.misc b/changelog.d/19461.misc new file mode 100644 index 00000000000..9c444ffe3c9 --- /dev/null +++ b/changelog.d/19461.misc @@ -0,0 +1 @@ +Better instrument `JoinRoomAliasServlet` with tracing. diff --git a/synapse/handlers/room_member.py b/synapse/handlers/room_member.py index a8935fded65..0c6be727169 100644 --- a/synapse/handlers/room_member.py +++ b/synapse/handlers/room_member.py @@ -49,6 +49,7 @@ from synapse.handlers.state_deltas import MatchChange, StateDeltasHandler from synapse.handlers.worker_lock import NEW_EVENT_DURING_PURGE_LOCK_NAME from synapse.logging import opentracing +from synapse.logging.opentracing import SynapseTags, set_tag, tag_args, trace from synapse.metrics import SERVER_NAME_LABEL, event_processing_positions from synapse.replication.http.push import ReplicationCopyPusherRestServlet from synapse.storage.databases.main.state_deltas import StateDelta @@ -390,6 +391,7 @@ async def ratelimit_invite( if requester is not None: await self._invites_per_issuer_limiter.ratelimit(requester) + @trace async def _local_membership_update( self, *, @@ -1221,6 +1223,8 @@ async def check_for_any_membership_in_room( if result is None or result == (None, None): raise AuthError(403, f"User {user_id} has no membership in room {room_id}") + @trace + @tag_args async def _should_perform_remote_join( self, user_id: str, @@ -1275,6 +1279,12 @@ async def _should_perform_remote_join( prev_member_event = await self.store.get_event(prev_member_event_id) previous_membership = prev_member_event.membership + # Interesting because it's used in the logic below to make decisions + set_tag( + SynapseTags.RESULT_PREFIX + "previous_membership", + str(previous_membership), + ) + # If we are not fully joined yet, and the target is not already in the room, # let's do a remote join so another server with the full state can validate # that the user has not been banned for example. @@ -1315,15 +1325,19 @@ async def _should_perform_remote_join( state_key: event_map[event_id] for state_key, event_id in state_before_join.items() } - allowed_servers = get_servers_from_users( + servers_that_can_issue_invite = get_servers_from_users( get_users_which_can_issue_invite(current_state) ) + set_tag( + SynapseTags.RESULT_PREFIX + "servers_that_can_issue_invite", + str(servers_that_can_issue_invite), + ) # If the local server is not one of allowed servers, then a remote # join must be done. Return the list of prospective servers based on # which can issue invites. - if self.hs.hostname not in allowed_servers: - return True, list(allowed_servers) + if self.hs.hostname not in servers_that_can_issue_invite: + return True, list(servers_that_can_issue_invite) # Ensure the member should be allowed access via membership in a room. await self.event_auth_handler.check_restricted_join_rules( @@ -1897,6 +1911,7 @@ async def _is_local_room_too_complex(self, room_id: str) -> bool: return complexity["v1"] > max_complexity + @trace async def _remote_join( self, requester: Requester, @@ -1975,6 +1990,7 @@ async def _remote_join( return event_id, stream_id + @trace async def remote_reject_invite( self, invite_event_id: str, @@ -2012,6 +2028,7 @@ async def remote_reject_invite( invite_event, txn_id, requester, content ) + @trace async def remote_rescind_knock( self, knock_event_id: str, @@ -2124,6 +2141,7 @@ async def _generate_local_out_of_band_leave( return result_event.event_id, result_event.internal_metadata.stream_ordering + @trace async def remote_knock( self, requester: Requester, diff --git a/synapse/handlers/room_member_worker.py b/synapse/handlers/room_member_worker.py index b56519ab0a1..da635255980 100644 --- a/synapse/handlers/room_member_worker.py +++ b/synapse/handlers/room_member_worker.py @@ -23,6 +23,7 @@ from typing import TYPE_CHECKING from synapse.handlers.room_member import NoKnownServersError, RoomMemberHandler +from synapse.logging.opentracing import trace from synapse.replication.http.membership import ( ReplicationRemoteJoinRestServlet as ReplRemoteJoin, ReplicationRemoteKnockRestServlet as ReplRemoteKnock, @@ -48,6 +49,7 @@ def __init__(self, hs: "HomeServer"): self._remote_rescind_client = ReplRescindKnock.make_client(hs) self._notify_change_client = ReplJoinedLeft.make_client(hs) + @trace async def _remote_join( self, requester: Requester, @@ -70,6 +72,7 @@ async def _remote_join( return ret["event_id"], ret["stream_id"] + @trace async def remote_reject_invite( self, invite_event_id: str, @@ -90,6 +93,7 @@ async def remote_reject_invite( ) return ret["event_id"], ret["stream_id"] + @trace async def remote_rescind_knock( self, knock_event_id: str, @@ -118,6 +122,7 @@ async def remote_rescind_knock( ) return ret["event_id"], ret["stream_id"] + @trace async def remote_knock( self, requester: Requester, From b2778dae700d89e5c048b0015275f29044de0522 Mon Sep 17 00:00:00 2001 From: Eric Eastwood Date: Wed, 18 Feb 2026 11:37:09 -0600 Subject: [PATCH 029/121] Fix Complement CI not running against the code from our PRs (remote images being chosen over local) (#19475) Fix remote images being chosen over the local ones we just built with Complement in CI (any Docker environment using the `containerd` image store). This problem means that Complement jobs in CI don't actually test against the code from the PR (since 2026-02-10). This PR approaches the problem the same way that @AndrewFerr proposed in https://github.com/element-hq/synapse/pull/18210. This is better than the alternative listed below as we can just make our code compatible with whatever image store is being used. ### Problem Spawning from https://github.com/element-hq/synapse/pull/19460#discussion_r2818760635 where we found that our Complement jobs in CI don't actually test against the code from the PR at the moment. This is caused by a change in Docker Engine 29.0.0: > `containerd` image store is now the default for **fresh installs**. This doesn't apply to daemons configured with `userns-remap` (see [moby#47377](https://github.com/moby/moby/issues/47377)). > > *-- 29.0.0 (2025-11-10), https://docs.docker.com/engine/release-notes/29/#2900* And our `ubuntu-latest` GitHub runner (`Current runner version: '2.331.0'`) [points](https://github.com/actions/runner-images/blob/ubuntu24/20260209.23/images/ubuntu/Ubuntu2404-Readme.md) to using Docker client/server `29.1.5` :dart: This Docker version bump happened on https://github.com/actions/runner-images/commit/416418df15a48321bdf51c3978c07a84a1873c75 (2026-02-10) (`28.0.4` -> `29.1.5`). Specific PR: https://github.com/actions/runner-images/pull/13633 --- I found this because I reviewed and remembered https://github.com/element-hq/synapse/pull/18210 was a thing that @AndrewFerr ran into. And then running `dockers system prune` also revealed the problematic `containerd` in CI. Checking the Docker changelogs, I found the new default culprit and then could trace down where the GitHub runners made the dependency update. --------- Co-authored-by: Andrew Ferrazzutti --- changelog.d/19475.misc | 1 + docker/README-testing.md | 8 ++--- scripts-dev/complement.sh | 71 ++++++++++++++++++++++++++------------- 3 files changed, 53 insertions(+), 27 deletions(-) create mode 100644 changelog.d/19475.misc diff --git a/changelog.d/19475.misc b/changelog.d/19475.misc new file mode 100644 index 00000000000..ea2774dcb15 --- /dev/null +++ b/changelog.d/19475.misc @@ -0,0 +1 @@ +Fix Complement CI not running against the code from our PRs. diff --git a/docker/README-testing.md b/docker/README-testing.md index 2ef8556d76f..aeeba58730a 100644 --- a/docker/README-testing.md +++ b/docker/README-testing.md @@ -31,23 +31,23 @@ release of Synapse, instead of your current checkout, you can skip this step. Fr root of the repository: ```sh -docker build -t matrixdotorg/synapse -f docker/Dockerfile . +docker build -t localhost/synapse -f docker/Dockerfile . ``` Next, build the workerised Synapse docker image, which is a layer over the base image. ```sh -docker build -t matrixdotorg/synapse-workers -f docker/Dockerfile-workers . +docker build -t localhost/synapse-workers --build-arg FROM=localhost/synapse -f docker/Dockerfile-workers . ``` Finally, build the multi-purpose image for Complement, which is a layer over the workers image. ```sh -docker build -t complement-synapse -f docker/complement/Dockerfile docker/complement +docker build -t localhost/complement-synapse -f docker/complement/Dockerfile --build-arg FROM=localhost/synapse-workers docker/complement ``` -This will build an image with the tag `complement-synapse`, which can be handed to +This will build an image with the tag `localhost/complement-synapse`, which can be handed to Complement for testing via the `COMPLEMENT_BASE_IMAGE` environment variable. Refer to [Complement's documentation](https://github.com/matrix-org/complement/#running) for how to run the tests, as well as the various available command line flags. diff --git a/scripts-dev/complement.sh b/scripts-dev/complement.sh index dc2262a2a35..9a18a621ee9 100755 --- a/scripts-dev/complement.sh +++ b/scripts-dev/complement.sh @@ -35,6 +35,26 @@ # Exit if a line returns a non-zero exit code 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 +# updated to use Docker Engine 29.0.0+ which uses `containerd` by default for new +# installations. +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" +COMPLEMENT_SYNAPSE_IMAGE_PATH="$LOCAL_IMAGE_NAMESPACE/complement-synapse" + +SYNAPSE_EDITABLE_IMAGE_PATH="$LOCAL_IMAGE_NAMESPACE/synapse-editable" +SYNAPSE_WORKERS_EDITABLE_IMAGE_PATH="$LOCAL_IMAGE_NAMESPACE/synapse-workers-editable" +COMPLEMENT_SYNAPSE_EDITABLE_IMAGE_PATH="$LOCAL_IMAGE_NAMESPACE/complement-synapse-editable" + # Helper to emit annotations that collapse portions of the log in GitHub Actions echo_if_github() { if [[ -n "$GITHUB_WORKFLOW" ]]; then @@ -53,7 +73,7 @@ Run the complement test suite on Synapse. -f, --fast Skip rebuilding the docker images, and just use the most recent - 'complement-synapse:latest' image. + 'localhost/complement-synapse:latest' image. Conflicts with --build-only. --build-only @@ -154,16 +174,16 @@ main() { editable_mount="$(realpath .):/editable-src:z" if [ -n "$rebuild_editable_synapse" ]; then unset skip_docker_build - elif $CONTAINER_RUNTIME inspect complement-synapse-editable &>/dev/null; then + elif $CONTAINER_RUNTIME inspect "$COMPLEMENT_SYNAPSE_EDITABLE_IMAGE_PATH" &>/dev/null; then # complement-synapse-editable already exists: see if we can still use it: # - The Rust module must still be importable; it will fail to import if the Rust source has changed. # - The Poetry lock file must be the same (otherwise we assume dependencies have changed) # First set up the module in the right place for an editable installation. - $CONTAINER_RUNTIME run --rm -v $editable_mount --entrypoint 'cp' complement-synapse-editable -- /synapse_rust.abi3.so.bak /editable-src/synapse/synapse_rust.abi3.so + $CONTAINER_RUNTIME run --rm -v $editable_mount --entrypoint 'cp' "$COMPLEMENT_SYNAPSE_EDITABLE_IMAGE_PATH" -- /synapse_rust.abi3.so.bak /editable-src/synapse/synapse_rust.abi3.so - if ($CONTAINER_RUNTIME run --rm -v $editable_mount --entrypoint 'python' complement-synapse-editable -c 'import synapse.synapse_rust' \ - && $CONTAINER_RUNTIME run --rm -v $editable_mount --entrypoint 'diff' complement-synapse-editable --brief /editable-src/poetry.lock /poetry.lock.bak); then + if ($CONTAINER_RUNTIME run --rm -v $editable_mount --entrypoint 'python' "$COMPLEMENT_SYNAPSE_EDITABLE_IMAGE_PATH" -c 'import synapse.synapse_rust' \ + && $CONTAINER_RUNTIME run --rm -v $editable_mount --entrypoint 'diff' "$COMPLEMENT_SYNAPSE_EDITABLE_IMAGE_PATH" --brief /editable-src/poetry.lock /poetry.lock.bak); then skip_docker_build=1 else echo "Editable Synapse image is stale. Will rebuild." @@ -177,42 +197,47 @@ main() { # Build a special image designed for use in development with editable # installs. - $CONTAINER_RUNTIME build -t synapse-editable \ + $CONTAINER_RUNTIME build \ + -t "$SYNAPSE_EDITABLE_IMAGE_PATH" \ -f "docker/editable.Dockerfile" . - $CONTAINER_RUNTIME build -t synapse-workers-editable \ - --build-arg FROM=synapse-editable \ + $CONTAINER_RUNTIME build \ + -t "$SYNAPSE_WORKERS_EDITABLE_IMAGE_PATH" \ + --build-arg FROM="$SYNAPSE_EDITABLE_IMAGE_PATH" \ -f "docker/Dockerfile-workers" . - $CONTAINER_RUNTIME build -t complement-synapse-editable \ - --build-arg FROM=synapse-workers-editable \ + $CONTAINER_RUNTIME build \ + -t "$COMPLEMENT_SYNAPSE_EDITABLE_IMAGE_PATH" \ + --build-arg FROM="$SYNAPSE_WORKERS_EDITABLE_IMAGE_PATH" \ -f "docker/complement/Dockerfile" "docker/complement" # Prepare the Rust module - $CONTAINER_RUNTIME run --rm -v $editable_mount --entrypoint 'cp' complement-synapse-editable -- /synapse_rust.abi3.so.bak /editable-src/synapse/synapse_rust.abi3.so + $CONTAINER_RUNTIME run --rm -v $editable_mount --entrypoint 'cp' "$COMPLEMENT_SYNAPSE_EDITABLE_IMAGE_PATH" -- /synapse_rust.abi3.so.bak /editable-src/synapse/synapse_rust.abi3.so else # Build the base Synapse image from the local checkout echo_if_github "::group::Build Docker image: matrixdotorg/synapse" - $CONTAINER_RUNTIME build -t matrixdotorg/synapse \ - --build-arg TEST_ONLY_SKIP_DEP_HASH_VERIFICATION \ - --build-arg TEST_ONLY_IGNORE_POETRY_LOCKFILE \ - -f "docker/Dockerfile" . + $CONTAINER_RUNTIME build \ + -t "$SYNAPSE_IMAGE_PATH" \ + --build-arg TEST_ONLY_SKIP_DEP_HASH_VERIFICATION \ + --build-arg TEST_ONLY_IGNORE_POETRY_LOCKFILE \ + -f "docker/Dockerfile" . echo_if_github "::endgroup::" # Build the workers docker image (from the base Synapse image we just built). echo_if_github "::group::Build Docker image: matrixdotorg/synapse-workers" - $CONTAINER_RUNTIME build -t matrixdotorg/synapse-workers -f "docker/Dockerfile-workers" . + $CONTAINER_RUNTIME build \ + -t "$SYNAPSE_WORKERS_IMAGE_PATH" \ + --build-arg FROM="$SYNAPSE_IMAGE_PATH" \ + -f "docker/Dockerfile-workers" . echo_if_github "::endgroup::" # Build the unified Complement image (from the worker Synapse image we just built). echo_if_github "::group::Build Docker image: complement/Dockerfile" - $CONTAINER_RUNTIME build -t complement-synapse \ - `# This is the tag we end up pushing to the registry (see` \ - `# .github/workflows/push_complement_image.yml) so let's just label it now` \ - `# so people can reference it by the same name locally.` \ - -t ghcr.io/element-hq/synapse/complement-synapse \ + $CONTAINER_RUNTIME build \ + -t "$COMPLEMENT_SYNAPSE_IMAGE_PATH" \ + --build-arg FROM="$SYNAPSE_WORKERS_IMAGE_PATH" \ -f "docker/complement/Dockerfile" "docker/complement" echo_if_github "::endgroup::" @@ -253,9 +278,9 @@ main() { ./tests/... ) - export COMPLEMENT_BASE_IMAGE=complement-synapse + export COMPLEMENT_BASE_IMAGE="$COMPLEMENT_SYNAPSE_IMAGE_PATH" if [ -n "$use_editable_synapse" ]; then - export COMPLEMENT_BASE_IMAGE=complement-synapse-editable + export COMPLEMENT_BASE_IMAGE="$COMPLEMENT_SYNAPSE_EDITABLE_IMAGE_PATH" export COMPLEMENT_HOST_MOUNTS="$editable_mount" fi From 04206aebdf2444f189a5744b5cb62d419ff83e8b Mon Sep 17 00:00:00 2001 From: Eric Eastwood Date: Thu, 19 Feb 2026 09:57:25 -0600 Subject: [PATCH 030/121] Log `docker system info` in CI (#19480) Follow-up to https://github.com/element-hq/synapse/pull/19460#discussion_r2819139638 and https://github.com/element-hq/synapse/pull/19475 --- .github/workflows/tests.yml | 6 ++++++ changelog.d/19480.misc | 1 + 2 files changed, 7 insertions(+) create mode 100644 changelog.d/19480.misc diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 81a72f1f688..98b47130a1c 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -693,6 +693,12 @@ jobs: with: path: synapse + # Log Docker system info for debugging (compare with your local environment) and + # tracking GitHub runner changes over time (can easily compare a run from last + # week with the current one in question). + - run: docker system info + shell: bash + - name: Install Rust uses: dtolnay/rust-toolchain@e97e2d8cc328f1b50210efc529dca0028893a2d9 # master with: diff --git a/changelog.d/19480.misc b/changelog.d/19480.misc new file mode 100644 index 00000000000..60abe04962c --- /dev/null +++ b/changelog.d/19480.misc @@ -0,0 +1 @@ +Log `docker system info` in CI so we have a plain record of how GitHub runners evolve over time. From 0ac772f082bc3adca0a99495f6805e3ea31fbcb3 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 20 Feb 2026 15:42:16 +0100 Subject: [PATCH 031/121] Bump pillow from 12.0.0 to 12.1.1 (#19454) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps [pillow](https://github.com/python-pillow/Pillow) from 12.0.0 to 12.1.1.
Release notes

Sourced from pillow's releases.

12.1.1

https://pillow.readthedocs.io/en/stable/releasenotes/12.1.1.html

Dependencies

Other changes

12.1.0

https://pillow.readthedocs.io/en/stable/releasenotes/12.1.0.html

Deprecations

Documentation

Dependencies

Testing

... (truncated)

Commits

[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=pillow&package-manager=pip&previous-version=12.0.0&new-version=12.1.1)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores) Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`. [//]: # (dependabot-automerge-start) [//]: # (dependabot-automerge-end) ---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot show ignore conditions` will show all of the ignore conditions of the specified dependency - `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself) You can disable automated security fix PRs for this repo from the [Security Alerts page](https://github.com/element-hq/synapse/network/alerts).
Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Quentin Gliech --- poetry.lock | 184 ++++++++++++++++++++++++++-------------------------- 1 file changed, 92 insertions(+), 92 deletions(-) diff --git a/poetry.lock b/poetry.lock index 5f1256676e9..3c80a4dfdc5 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1704,103 +1704,103 @@ files = [ [[package]] name = "pillow" -version = "12.0.0" +version = "12.1.1" description = "Python Imaging Library (fork)" optional = false python-versions = ">=3.10" groups = ["main"] files = [ - {file = "pillow-12.0.0-cp310-cp310-macosx_10_10_x86_64.whl", hash = "sha256:3adfb466bbc544b926d50fe8f4a4e6abd8c6bffd28a26177594e6e9b2b76572b"}, - {file = "pillow-12.0.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:1ac11e8ea4f611c3c0147424eae514028b5e9077dd99ab91e1bd7bc33ff145e1"}, - {file = "pillow-12.0.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d49e2314c373f4c2b39446fb1a45ed333c850e09d0c59ac79b72eb3b95397363"}, - {file = "pillow-12.0.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:c7b2a63fd6d5246349f3d3f37b14430d73ee7e8173154461785e43036ffa96ca"}, - {file = "pillow-12.0.0-cp310-cp310-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d64317d2587c70324b79861babb9c09f71fbb780bad212018874b2c013d8600e"}, - {file = "pillow-12.0.0-cp310-cp310-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d77153e14b709fd8b8af6f66a3afbb9ed6e9fc5ccf0b6b7e1ced7b036a228782"}, - {file = "pillow-12.0.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:32ed80ea8a90ee3e6fa08c21e2e091bba6eda8eccc83dbc34c95169507a91f10"}, - {file = "pillow-12.0.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:c828a1ae702fc712978bda0320ba1b9893d99be0badf2647f693cc01cf0f04fa"}, - {file = "pillow-12.0.0-cp310-cp310-win32.whl", hash = "sha256:bd87e140e45399c818fac4247880b9ce719e4783d767e030a883a970be632275"}, - {file = "pillow-12.0.0-cp310-cp310-win_amd64.whl", hash = "sha256:455247ac8a4cfb7b9bc45b7e432d10421aea9fc2e74d285ba4072688a74c2e9d"}, - {file = "pillow-12.0.0-cp310-cp310-win_arm64.whl", hash = "sha256:6ace95230bfb7cd79ef66caa064bbe2f2a1e63d93471c3a2e1f1348d9f22d6b7"}, - {file = "pillow-12.0.0-cp311-cp311-macosx_10_10_x86_64.whl", hash = "sha256:0fd00cac9c03256c8b2ff58f162ebcd2587ad3e1f2e397eab718c47e24d231cc"}, - {file = "pillow-12.0.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:a3475b96f5908b3b16c47533daaa87380c491357d197564e0ba34ae75c0f3257"}, - {file = "pillow-12.0.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:110486b79f2d112cf6add83b28b627e369219388f64ef2f960fef9ebaf54c642"}, - {file = "pillow-12.0.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:5269cc1caeedb67e6f7269a42014f381f45e2e7cd42d834ede3c703a1d915fe3"}, - {file = "pillow-12.0.0-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:aa5129de4e174daccbc59d0a3b6d20eaf24417d59851c07ebb37aeb02947987c"}, - {file = "pillow-12.0.0-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bee2a6db3a7242ea309aa7ee8e2780726fed67ff4e5b40169f2c940e7eb09227"}, - {file = "pillow-12.0.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:90387104ee8400a7b4598253b4c406f8958f59fcf983a6cea2b50d59f7d63d0b"}, - {file = "pillow-12.0.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:bc91a56697869546d1b8f0a3ff35224557ae7f881050e99f615e0119bf934b4e"}, - {file = "pillow-12.0.0-cp311-cp311-win32.whl", hash = "sha256:27f95b12453d165099c84f8a8bfdfd46b9e4bda9e0e4b65f0635430027f55739"}, - {file = "pillow-12.0.0-cp311-cp311-win_amd64.whl", hash = "sha256:b583dc9070312190192631373c6c8ed277254aa6e6084b74bdd0a6d3b221608e"}, - {file = "pillow-12.0.0-cp311-cp311-win_arm64.whl", hash = "sha256:759de84a33be3b178a64c8ba28ad5c135900359e85fb662bc6e403ad4407791d"}, - {file = "pillow-12.0.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:53561a4ddc36facb432fae7a9d8afbfaf94795414f5cdc5fc52f28c1dca90371"}, - {file = "pillow-12.0.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:71db6b4c1653045dacc1585c1b0d184004f0d7e694c7b34ac165ca70c0838082"}, - {file = "pillow-12.0.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:2fa5f0b6716fc88f11380b88b31fe591a06c6315e955c096c35715788b339e3f"}, - {file = "pillow-12.0.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:82240051c6ca513c616f7f9da06e871f61bfd7805f566275841af15015b8f98d"}, - {file = "pillow-12.0.0-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:55f818bd74fe2f11d4d7cbc65880a843c4075e0ac7226bc1a23261dbea531953"}, - {file = "pillow-12.0.0-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b87843e225e74576437fd5b6a4c2205d422754f84a06942cfaf1dc32243e45a8"}, - {file = "pillow-12.0.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:c607c90ba67533e1b2355b821fef6764d1dd2cbe26b8c1005ae84f7aea25ff79"}, - {file = "pillow-12.0.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:21f241bdd5080a15bc86d3466a9f6074a9c2c2b314100dd896ac81ee6db2f1ba"}, - {file = "pillow-12.0.0-cp312-cp312-win32.whl", hash = "sha256:dd333073e0cacdc3089525c7df7d39b211bcdf31fc2824e49d01c6b6187b07d0"}, - {file = "pillow-12.0.0-cp312-cp312-win_amd64.whl", hash = "sha256:9fe611163f6303d1619bbcb653540a4d60f9e55e622d60a3108be0d5b441017a"}, - {file = "pillow-12.0.0-cp312-cp312-win_arm64.whl", hash = "sha256:7dfb439562f234f7d57b1ac6bc8fe7f838a4bd49c79230e0f6a1da93e82f1fad"}, - {file = "pillow-12.0.0-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:0869154a2d0546545cde61d1789a6524319fc1897d9ee31218eae7a60ccc5643"}, - {file = "pillow-12.0.0-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:a7921c5a6d31b3d756ec980f2f47c0cfdbce0fc48c22a39347a895f41f4a6ea4"}, - {file = "pillow-12.0.0-cp313-cp313-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:1ee80a59f6ce048ae13cda1abf7fbd2a34ab9ee7d401c46be3ca685d1999a399"}, - {file = "pillow-12.0.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:c50f36a62a22d350c96e49ad02d0da41dbd17ddc2e29750dbdba4323f85eb4a5"}, - {file = "pillow-12.0.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:5193fde9a5f23c331ea26d0cf171fbf67e3f247585f50c08b3e205c7aeb4589b"}, - {file = "pillow-12.0.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:bde737cff1a975b70652b62d626f7785e0480918dece11e8fef3c0cf057351c3"}, - {file = "pillow-12.0.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:a6597ff2b61d121172f5844b53f21467f7082f5fb385a9a29c01414463f93b07"}, - {file = "pillow-12.0.0-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0b817e7035ea7f6b942c13aa03bb554fc44fea70838ea21f8eb31c638326584e"}, - {file = "pillow-12.0.0-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f4f1231b7dec408e8670264ce63e9c71409d9583dd21d32c163e25213ee2a344"}, - {file = "pillow-12.0.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:6e51b71417049ad6ab14c49608b4a24d8fb3fe605e5dfabfe523b58064dc3d27"}, - {file = "pillow-12.0.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:d120c38a42c234dc9a8c5de7ceaaf899cf33561956acb4941653f8bdc657aa79"}, - {file = "pillow-12.0.0-cp313-cp313-win32.whl", hash = "sha256:4cc6b3b2efff105c6a1656cfe59da4fdde2cda9af1c5e0b58529b24525d0a098"}, - {file = "pillow-12.0.0-cp313-cp313-win_amd64.whl", hash = "sha256:4cf7fed4b4580601c4345ceb5d4cbf5a980d030fd5ad07c4d2ec589f95f09905"}, - {file = "pillow-12.0.0-cp313-cp313-win_arm64.whl", hash = "sha256:9f0b04c6b8584c2c193babcccc908b38ed29524b29dd464bc8801bf10d746a3a"}, - {file = "pillow-12.0.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:7fa22993bac7b77b78cae22bad1e2a987ddf0d9015c63358032f84a53f23cdc3"}, - {file = "pillow-12.0.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:f135c702ac42262573fe9714dfe99c944b4ba307af5eb507abef1667e2cbbced"}, - {file = "pillow-12.0.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:c85de1136429c524e55cfa4e033b4a7940ac5c8ee4d9401cc2d1bf48154bbc7b"}, - {file = "pillow-12.0.0-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:38df9b4bfd3db902c9c2bd369bcacaf9d935b2fff73709429d95cc41554f7b3d"}, - {file = "pillow-12.0.0-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7d87ef5795da03d742bf49439f9ca4d027cde49c82c5371ba52464aee266699a"}, - {file = "pillow-12.0.0-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:aff9e4d82d082ff9513bdd6acd4f5bd359f5b2c870907d2b0a9c5e10d40c88fe"}, - {file = "pillow-12.0.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:8d8ca2b210ada074d57fcee40c30446c9562e542fc46aedc19baf758a93532ee"}, - {file = "pillow-12.0.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:99a7f72fb6249302aa62245680754862a44179b545ded638cf1fef59befb57ef"}, - {file = "pillow-12.0.0-cp313-cp313t-win32.whl", hash = "sha256:4078242472387600b2ce8d93ade8899c12bf33fa89e55ec89fe126e9d6d5d9e9"}, - {file = "pillow-12.0.0-cp313-cp313t-win_amd64.whl", hash = "sha256:2c54c1a783d6d60595d3514f0efe9b37c8808746a66920315bfd34a938d7994b"}, - {file = "pillow-12.0.0-cp313-cp313t-win_arm64.whl", hash = "sha256:26d9f7d2b604cd23aba3e9faf795787456ac25634d82cd060556998e39c6fa47"}, - {file = "pillow-12.0.0-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:beeae3f27f62308f1ddbcfb0690bf44b10732f2ef43758f169d5e9303165d3f9"}, - {file = "pillow-12.0.0-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:d4827615da15cd59784ce39d3388275ec093ae3ee8d7f0c089b76fa87af756c2"}, - {file = "pillow-12.0.0-cp314-cp314-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:3e42edad50b6909089750e65c91aa09aaf1e0a71310d383f11321b27c224ed8a"}, - {file = "pillow-12.0.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:e5d8efac84c9afcb40914ab49ba063d94f5dbdf5066db4482c66a992f47a3a3b"}, - {file = "pillow-12.0.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:266cd5f2b63ff316d5a1bba46268e603c9caf5606d44f38c2873c380950576ad"}, - {file = "pillow-12.0.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:58eea5ebe51504057dd95c5b77d21700b77615ab0243d8152793dc00eb4faf01"}, - {file = "pillow-12.0.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:f13711b1a5ba512d647a0e4ba79280d3a9a045aaf7e0cc6fbe96b91d4cdf6b0c"}, - {file = "pillow-12.0.0-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6846bd2d116ff42cba6b646edf5bf61d37e5cbd256425fa089fee4ff5c07a99e"}, - {file = "pillow-12.0.0-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c98fa880d695de164b4135a52fd2e9cd7b7c90a9d8ac5e9e443a24a95ef9248e"}, - {file = "pillow-12.0.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:fa3ed2a29a9e9d2d488b4da81dcb54720ac3104a20bf0bd273f1e4648aff5af9"}, - {file = "pillow-12.0.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:d034140032870024e6b9892c692fe2968493790dd57208b2c37e3fb35f6df3ab"}, - {file = "pillow-12.0.0-cp314-cp314-win32.whl", hash = "sha256:1b1b133e6e16105f524a8dec491e0586d072948ce15c9b914e41cdadd209052b"}, - {file = "pillow-12.0.0-cp314-cp314-win_amd64.whl", hash = "sha256:8dc232e39d409036af549c86f24aed8273a40ffa459981146829a324e0848b4b"}, - {file = "pillow-12.0.0-cp314-cp314-win_arm64.whl", hash = "sha256:d52610d51e265a51518692045e372a4c363056130d922a7351429ac9f27e70b0"}, - {file = "pillow-12.0.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:1979f4566bb96c1e50a62d9831e2ea2d1211761e5662afc545fa766f996632f6"}, - {file = "pillow-12.0.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:b2e4b27a6e15b04832fe9bf292b94b5ca156016bbc1ea9c2c20098a0320d6cf6"}, - {file = "pillow-12.0.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:fb3096c30df99fd01c7bf8e544f392103d0795b9f98ba71a8054bcbf56b255f1"}, - {file = "pillow-12.0.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:7438839e9e053ef79f7112c881cef684013855016f928b168b81ed5835f3e75e"}, - {file = "pillow-12.0.0-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5d5c411a8eaa2299322b647cd932586b1427367fd3184ffbb8f7a219ea2041ca"}, - {file = "pillow-12.0.0-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d7e091d464ac59d2c7ad8e7e08105eaf9dafbc3883fd7265ffccc2baad6ac925"}, - {file = "pillow-12.0.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:792a2c0be4dcc18af9d4a2dfd8a11a17d5e25274a1062b0ec1c2d79c76f3e7f8"}, - {file = "pillow-12.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:afbefa430092f71a9593a99ab6a4e7538bc9eabbf7bf94f91510d3503943edc4"}, - {file = "pillow-12.0.0-cp314-cp314t-win32.whl", hash = "sha256:3830c769decf88f1289680a59d4f4c46c72573446352e2befec9a8512104fa52"}, - {file = "pillow-12.0.0-cp314-cp314t-win_amd64.whl", hash = "sha256:905b0365b210c73afb0ebe9101a32572152dfd1c144c7e28968a331b9217b94a"}, - {file = "pillow-12.0.0-cp314-cp314t-win_arm64.whl", hash = "sha256:99353a06902c2e43b43e8ff74ee65a7d90307d82370604746738a1e0661ccca7"}, - {file = "pillow-12.0.0-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:b22bd8c974942477156be55a768f7aa37c46904c175be4e158b6a86e3a6b7ca8"}, - {file = "pillow-12.0.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:805ebf596939e48dbb2e4922a1d3852cfc25c38160751ce02da93058b48d252a"}, - {file = "pillow-12.0.0-pp311-pypy311_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:cae81479f77420d217def5f54b5b9d279804d17e982e0f2fa19b1d1e14ab5197"}, - {file = "pillow-12.0.0-pp311-pypy311_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:aeaefa96c768fc66818730b952a862235d68825c178f1b3ffd4efd7ad2edcb7c"}, - {file = "pillow-12.0.0-pp311-pypy311_pp73-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:09f2d0abef9e4e2f349305a4f8cc784a8a6c2f58a8c4892eea13b10a943bd26e"}, - {file = "pillow-12.0.0-pp311-pypy311_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bdee52571a343d721fb2eb3b090a82d959ff37fc631e3f70422e0c2e029f3e76"}, - {file = "pillow-12.0.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:b290fd8aa38422444d4b50d579de197557f182ef1068b75f5aa8558638b8d0a5"}, - {file = "pillow-12.0.0.tar.gz", hash = "sha256:87d4f8125c9988bfbed67af47dd7a953e2fc7b0cc1e7800ec6d2080d490bb353"}, + {file = "pillow-12.1.1-cp310-cp310-macosx_10_10_x86_64.whl", hash = "sha256:1f1625b72740fdda5d77b4def688eb8fd6490975d06b909fd19f13f391e077e0"}, + {file = "pillow-12.1.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:178aa072084bd88ec759052feca8e56cbb14a60b39322b99a049e58090479713"}, + {file = "pillow-12.1.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:b66e95d05ba806247aaa1561f080abc7975daf715c30780ff92a20e4ec546e1b"}, + {file = "pillow-12.1.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:89c7e895002bbe49cdc5426150377cbbc04767d7547ed145473f496dfa40408b"}, + {file = "pillow-12.1.1-cp310-cp310-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3a5cbdcddad0af3da87cb16b60d23648bc3b51967eb07223e9fed77a82b457c4"}, + {file = "pillow-12.1.1-cp310-cp310-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9f51079765661884a486727f0729d29054242f74b46186026582b4e4769918e4"}, + {file = "pillow-12.1.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:99c1506ea77c11531d75e3a412832a13a71c7ebc8192ab9e4b2e355555920e3e"}, + {file = "pillow-12.1.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:36341d06738a9f66c8287cf8b876d24b18db9bd8740fa0672c74e259ad408cff"}, + {file = "pillow-12.1.1-cp310-cp310-win32.whl", hash = "sha256:6c52f062424c523d6c4db85518774cc3d50f5539dd6eed32b8f6229b26f24d40"}, + {file = "pillow-12.1.1-cp310-cp310-win_amd64.whl", hash = "sha256:c6008de247150668a705a6338156efb92334113421ceecf7438a12c9a12dab23"}, + {file = "pillow-12.1.1-cp310-cp310-win_arm64.whl", hash = "sha256:1a9b0ee305220b392e1124a764ee4265bd063e54a751a6b62eff69992f457fa9"}, + {file = "pillow-12.1.1-cp311-cp311-macosx_10_10_x86_64.whl", hash = "sha256:e879bb6cd5c73848ef3b2b48b8af9ff08c5b71ecda8048b7dd22d8a33f60be32"}, + {file = "pillow-12.1.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:365b10bb9417dd4498c0e3b128018c4a624dc11c7b97d8cc54effe3b096f4c38"}, + {file = "pillow-12.1.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d4ce8e329c93845720cd2014659ca67eac35f6433fd3050393d85f3ecef0dad5"}, + {file = "pillow-12.1.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:fc354a04072b765eccf2204f588a7a532c9511e8b9c7f900e1b64e3e33487090"}, + {file = "pillow-12.1.1-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7e7976bf1910a8116b523b9f9f58bf410f3e8aa330cd9a2bb2953f9266ab49af"}, + {file = "pillow-12.1.1-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:597bd9c8419bc7c6af5604e55847789b69123bbe25d65cc6ad3012b4f3c98d8b"}, + {file = "pillow-12.1.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:2c1fc0f2ca5f96a3c8407e41cca26a16e46b21060fe6d5b099d2cb01412222f5"}, + {file = "pillow-12.1.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:578510d88c6229d735855e1f278aa305270438d36a05031dfaae5067cc8eb04d"}, + {file = "pillow-12.1.1-cp311-cp311-win32.whl", hash = "sha256:7311c0a0dcadb89b36b7025dfd8326ecfa36964e29913074d47382706e516a7c"}, + {file = "pillow-12.1.1-cp311-cp311-win_amd64.whl", hash = "sha256:fbfa2a7c10cc2623f412753cddf391c7f971c52ca40a3f65dc5039b2939e8563"}, + {file = "pillow-12.1.1-cp311-cp311-win_arm64.whl", hash = "sha256:b81b5e3511211631b3f672a595e3221252c90af017e399056d0faabb9538aa80"}, + {file = "pillow-12.1.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:ab323b787d6e18b3d91a72fc99b1a2c28651e4358749842b8f8dfacd28ef2052"}, + {file = "pillow-12.1.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:adebb5bee0f0af4909c30db0d890c773d1a92ffe83da908e2e9e720f8edf3984"}, + {file = "pillow-12.1.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:bb66b7cc26f50977108790e2456b7921e773f23db5630261102233eb355a3b79"}, + {file = "pillow-12.1.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:aee2810642b2898bb187ced9b349e95d2a7272930796e022efaf12e99dccd293"}, + {file = "pillow-12.1.1-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a0b1cd6232e2b618adcc54d9882e4e662a089d5768cd188f7c245b4c8c44a397"}, + {file = "pillow-12.1.1-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7aac39bcf8d4770d089588a2e1dd111cbaa42df5a94be3114222057d68336bd0"}, + {file = "pillow-12.1.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:ab174cd7d29a62dd139c44bf74b698039328f45cb03b4596c43473a46656b2f3"}, + {file = "pillow-12.1.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:339ffdcb7cbeaa08221cd401d517d4b1fe7a9ed5d400e4a8039719238620ca35"}, + {file = "pillow-12.1.1-cp312-cp312-win32.whl", hash = "sha256:5d1f9575a12bed9e9eedd9a4972834b08c97a352bd17955ccdebfeca5913fa0a"}, + {file = "pillow-12.1.1-cp312-cp312-win_amd64.whl", hash = "sha256:21329ec8c96c6e979cd0dfd29406c40c1d52521a90544463057d2aaa937d66a6"}, + {file = "pillow-12.1.1-cp312-cp312-win_arm64.whl", hash = "sha256:af9a332e572978f0218686636610555ae3defd1633597be015ed50289a03c523"}, + {file = "pillow-12.1.1-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:d242e8ac078781f1de88bf823d70c1a9b3c7950a44cdf4b7c012e22ccbcd8e4e"}, + {file = "pillow-12.1.1-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:02f84dfad02693676692746df05b89cf25597560db2857363a208e393429f5e9"}, + {file = "pillow-12.1.1-cp313-cp313-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:e65498daf4b583091ccbb2556c7000abf0f3349fcd57ef7adc9a84a394ed29f6"}, + {file = "pillow-12.1.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:6c6db3b84c87d48d0088943bf33440e0c42370b99b1c2a7989216f7b42eede60"}, + {file = "pillow-12.1.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:8b7e5304e34942bf62e15184219a7b5ad4ff7f3bb5cca4d984f37df1a0e1aee2"}, + {file = "pillow-12.1.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:18e5bddd742a44b7e6b1e773ab5db102bd7a94c32555ba656e76d319d19c3850"}, + {file = "pillow-12.1.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:fc44ef1f3de4f45b50ccf9136999d71abb99dca7706bc75d222ed350b9fd2289"}, + {file = "pillow-12.1.1-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5a8eb7ed8d4198bccbd07058416eeec51686b498e784eda166395a23eb99138e"}, + {file = "pillow-12.1.1-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:47b94983da0c642de92ced1702c5b6c292a84bd3a8e1d1702ff923f183594717"}, + {file = "pillow-12.1.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:518a48c2aab7ce596d3bf79d0e275661b846e86e4d0e7dec34712c30fe07f02a"}, + {file = "pillow-12.1.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a550ae29b95c6dc13cf69e2c9dc5747f814c54eeb2e32d683e5e93af56caa029"}, + {file = "pillow-12.1.1-cp313-cp313-win32.whl", hash = "sha256:a003d7422449f6d1e3a34e3dd4110c22148336918ddbfc6a32581cd54b2e0b2b"}, + {file = "pillow-12.1.1-cp313-cp313-win_amd64.whl", hash = "sha256:344cf1e3dab3be4b1fa08e449323d98a2a3f819ad20f4b22e77a0ede31f0faa1"}, + {file = "pillow-12.1.1-cp313-cp313-win_arm64.whl", hash = "sha256:5c0dd1636633e7e6a0afe7bf6a51a14992b7f8e60de5789018ebbdfae55b040a"}, + {file = "pillow-12.1.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:0330d233c1a0ead844fc097a7d16c0abff4c12e856c0b325f231820fee1f39da"}, + {file = "pillow-12.1.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:5dae5f21afb91322f2ff791895ddd8889e5e947ff59f71b46041c8ce6db790bc"}, + {file = "pillow-12.1.1-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:2e0c664be47252947d870ac0d327fea7e63985a08794758aa8af5b6cb6ec0c9c"}, + {file = "pillow-12.1.1-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:691ab2ac363b8217f7d31b3497108fb1f50faab2f75dfb03284ec2f217e87bf8"}, + {file = "pillow-12.1.1-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e9e8064fb1cc019296958595f6db671fba95209e3ceb0c4734c9baf97de04b20"}, + {file = "pillow-12.1.1-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:472a8d7ded663e6162dafdf20015c486a7009483ca671cece7a9279b512fcb13"}, + {file = "pillow-12.1.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:89b54027a766529136a06cfebeecb3a04900397a3590fd252160b888479517bf"}, + {file = "pillow-12.1.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:86172b0831b82ce4f7877f280055892b31179e1576aa00d0df3bb1bbf8c3e524"}, + {file = "pillow-12.1.1-cp313-cp313t-win32.whl", hash = "sha256:44ce27545b6efcf0fdbdceb31c9a5bdea9333e664cda58a7e674bb74608b3986"}, + {file = "pillow-12.1.1-cp313-cp313t-win_amd64.whl", hash = "sha256:a285e3eb7a5a45a2ff504e31f4a8d1b12ef62e84e5411c6804a42197c1cf586c"}, + {file = "pillow-12.1.1-cp313-cp313t-win_arm64.whl", hash = "sha256:cc7d296b5ea4d29e6570dabeaed58d31c3fea35a633a69679fb03d7664f43fb3"}, + {file = "pillow-12.1.1-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:417423db963cb4be8bac3fc1204fe61610f6abeed1580a7a2cbb2fbda20f12af"}, + {file = "pillow-12.1.1-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:b957b71c6b2387610f556a7eb0828afbe40b4a98036fc0d2acfa5a44a0c2036f"}, + {file = "pillow-12.1.1-cp314-cp314-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:097690ba1f2efdeb165a20469d59d8bb03c55fb6621eb2041a060ae8ea3e9642"}, + {file = "pillow-12.1.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:2815a87ab27848db0321fb78c7f0b2c8649dee134b7f2b80c6a45c6831d75ccd"}, + {file = "pillow-12.1.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:f7ed2c6543bad5a7d5530eb9e78c53132f93dfa44a28492db88b41cdab885202"}, + {file = "pillow-12.1.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:652a2c9ccfb556235b2b501a3a7cf3742148cd22e04b5625c5fe057ea3e3191f"}, + {file = "pillow-12.1.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d6e4571eedf43af33d0fc233a382a76e849badbccdf1ac438841308652a08e1f"}, + {file = "pillow-12.1.1-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b574c51cf7d5d62e9be37ba446224b59a2da26dc4c1bb2ecbe936a4fb1a7cb7f"}, + {file = "pillow-12.1.1-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a37691702ed687799de29a518d63d4682d9016932db66d4e90c345831b02fb4e"}, + {file = "pillow-12.1.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:f95c00d5d6700b2b890479664a06e754974848afaae5e21beb4d83c106923fd0"}, + {file = "pillow-12.1.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:559b38da23606e68681337ad74622c4dbba02254fc9cb4488a305dd5975c7eeb"}, + {file = "pillow-12.1.1-cp314-cp314-win32.whl", hash = "sha256:03edcc34d688572014ff223c125a3f77fb08091e4607e7745002fc214070b35f"}, + {file = "pillow-12.1.1-cp314-cp314-win_amd64.whl", hash = "sha256:50480dcd74fa63b8e78235957d302d98d98d82ccbfac4c7e12108ba9ecbdba15"}, + {file = "pillow-12.1.1-cp314-cp314-win_arm64.whl", hash = "sha256:5cb1785d97b0c3d1d1a16bc1d710c4a0049daefc4935f3a8f31f827f4d3d2e7f"}, + {file = "pillow-12.1.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:1f90cff8aa76835cba5769f0b3121a22bd4eb9e6884cfe338216e557a9a548b8"}, + {file = "pillow-12.1.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:1f1be78ce9466a7ee64bfda57bdba0f7cc499d9794d518b854816c41bf0aa4e9"}, + {file = "pillow-12.1.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:42fc1f4677106188ad9a55562bbade416f8b55456f522430fadab3cef7cd4e60"}, + {file = "pillow-12.1.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:98edb152429ab62a1818039744d8fbb3ccab98a7c29fc3d5fcef158f3f1f68b7"}, + {file = "pillow-12.1.1-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d470ab1178551dd17fdba0fef463359c41aaa613cdcd7ff8373f54be629f9f8f"}, + {file = "pillow-12.1.1-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6408a7b064595afcab0a49393a413732a35788f2a5092fdc6266952ed67de586"}, + {file = "pillow-12.1.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:5d8c41325b382c07799a3682c1c258469ea2ff97103c53717b7893862d0c98ce"}, + {file = "pillow-12.1.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:c7697918b5be27424e9ce568193efd13d925c4481dd364e43f5dff72d33e10f8"}, + {file = "pillow-12.1.1-cp314-cp314t-win32.whl", hash = "sha256:d2912fd8114fc5545aa3a4b5576512f64c55a03f3ebcca4c10194d593d43ea36"}, + {file = "pillow-12.1.1-cp314-cp314t-win_amd64.whl", hash = "sha256:4ceb838d4bd9dab43e06c363cab2eebf63846d6a4aeaea283bbdfd8f1a8ed58b"}, + {file = "pillow-12.1.1-cp314-cp314t-win_arm64.whl", hash = "sha256:7b03048319bfc6170e93bd60728a1af51d3dd7704935feb228c4d4faab35d334"}, + {file = "pillow-12.1.1-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:600fd103672b925fe62ed08e0d874ea34d692474df6f4bf7ebe148b30f89f39f"}, + {file = "pillow-12.1.1-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:665e1b916b043cef294bc54d47bf02d87e13f769bc4bc5fa225a24b3a6c5aca9"}, + {file = "pillow-12.1.1-pp311-pypy311_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:495c302af3aad1ca67420ddd5c7bd480c8867ad173528767d906428057a11f0e"}, + {file = "pillow-12.1.1-pp311-pypy311_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:8fd420ef0c52c88b5a035a0886f367748c72147b2b8f384c9d12656678dfdfa9"}, + {file = "pillow-12.1.1-pp311-pypy311_pp73-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f975aa7ef9684ce7e2c18a3aa8f8e2106ce1e46b94ab713d156b2898811651d3"}, + {file = "pillow-12.1.1-pp311-pypy311_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8089c852a56c2966cf18835db62d9b34fef7ba74c726ad943928d494fa7f4735"}, + {file = "pillow-12.1.1-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:cb9bb857b2d057c6dfc72ac5f3b44836924ba15721882ef103cecb40d002d80e"}, + {file = "pillow-12.1.1.tar.gz", hash = "sha256:9ad8fa5937ab05218e2b6a4cff30295ad35afd2f83ac592e68c0d871bb0fdbc4"}, ] [package.extras] From 9b738d2ec5006e9d55eb23aa2d34f20f75ff1651 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 20 Feb 2026 15:14:46 +0000 Subject: [PATCH 032/121] Bump the patches group with 2 updates (#19488) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps the patches group with 2 updates: [anyhow](https://github.com/dtolnay/anyhow) and [regex](https://github.com/rust-lang/regex). Updates `anyhow` from 1.0.100 to 1.0.101
Release notes

Sourced from anyhow's releases.

1.0.101

Commits
  • 80bfe29 Release 1.0.101
  • dff8c43 Merge pull request #437 from Ibitier/inline-ok-helper
  • 85d9ea9 Add #[inline] to anyhow::Ok helper
  • 54036cc Update ui test suite to nightly-2026-01-21
  • cce0579 Update actions/upload-artifact@v5 -> v6
  • f2c598c Update actions/upload-artifact@v4 -> v5
  • 2c0bda4 Update to 2021 edition
  • 0d82268 Remove rustc version requirement from readme
  • 67df012 Merge pull request #436 from dtolnay/up
  • c898488 Raise required compiler to Rust 1.68
  • Additional commits viewable in compare view

Updates `regex` from 1.12.2 to 1.12.3
Changelog

Sourced from regex's changelog.

1.12.3 (2025-02-03)

This release excludes some unnecessary things from the archive published to crates.io. Specifically, fuzzing data and various shell scripts are now excluded. If you run into problems, please file an issue.

Improvements:

  • #1319: Switch from a Cargo exclude list to an include list, and exclude some unnecessary stuff.
Commits
  • b028e4f 1.12.3
  • 5e195de regex-automata-0.4.14
  • a3433f6 regex-syntax-0.8.9
  • 0c07fae regex-lite-0.1.9
  • 6a81006 cargo: exclude development scripts and fuzzing data
  • 4733e28 automata: fix onepass::DFA::try_search_slots panic when too many slots are ...
  • See full diff in compare view

Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`. [//]: # (dependabot-automerge-start) [//]: # (dependabot-automerge-end) ---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot show ignore conditions` will show all of the ignore conditions of the specified dependency - `@dependabot ignore major version` will close this group update PR and stop Dependabot creating any more for the specific dependency's major version (unless you unignore this specific dependency's major version or upgrade to it yourself) - `@dependabot ignore minor version` will close this group update PR and stop Dependabot creating any more for the specific dependency's minor version (unless you unignore this specific dependency's minor version or upgrade to it yourself) - `@dependabot ignore ` will close this group update PR and stop Dependabot creating any more for the specific dependency (unless you unignore this specific dependency or upgrade to it yourself) - `@dependabot unignore ` will remove all of the ignore conditions of the specified dependency - `@dependabot unignore ` will remove the ignore condition of the specified dependency and ignore conditions
Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- Cargo.lock | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 47f81eaef1d..15342020c90 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -13,9 +13,9 @@ dependencies = [ [[package]] name = "anyhow" -version = "1.0.100" +version = "1.0.101" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61" +checksum = "5f0e0fee31ef5ed1ba1316088939cea399010ed7731dba877ed44aeb407a75ea" [[package]] name = "arc-swap" @@ -995,9 +995,9 @@ dependencies = [ [[package]] name = "regex" -version = "1.12.2" +version = "1.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "843bc0191f75f3e22651ae5f1e72939ab2f72a4bc30fa80a066bd66edefc24d4" +checksum = "e10754a14b9137dd7b1e3e5b0493cc9171fdd105e0ab477f51b72e7f3ac0e276" dependencies = [ "aho-corasick", "memchr", From 45006524591258434119e1d46ba65f969481ce71 Mon Sep 17 00:00:00 2001 From: Olivier 'reivilibre Date: Fri, 20 Feb 2026 15:37:34 +0000 Subject: [PATCH 033/121] Rename the `test_disconnect` test helper so that pytest doesn't see it as a test. (#19486) This fixes one of the 2 blockers to using pytest instead of Trial (which is not formally-motivated, but sometimes seems like an interesting idea because pytest has seen a lot of developer experience features that Trial hasn't. It would also removes one more coupling to the Twisted framework.) --- The `test_` prefix to this test helper makes it appear as a test to pytest. We *can* set a `__test__ = False` attribute on the test, but it felt cleaner to just rename it (as I also thought it would be a test from that name!). This was previously reported as: https://github.com/element-hq/synapse/issues/18665 --------- Signed-off-by: Olivier 'reivilibre --- changelog.d/19486.misc | 1 + tests/federation/transport/server/test__base.py | 6 +++--- tests/http/server/_base.py | 2 +- tests/http/test_servlet.py | 6 +++--- tests/replication/http/test__base.py | 6 +++--- tests/test_server.py | 10 +++++----- 6 files changed, 16 insertions(+), 15 deletions(-) create mode 100644 changelog.d/19486.misc diff --git a/changelog.d/19486.misc b/changelog.d/19486.misc new file mode 100644 index 00000000000..c0c2fef770c --- /dev/null +++ b/changelog.d/19486.misc @@ -0,0 +1 @@ +Rename the `test_disconnect` test helper so that pytest doesn't see it as a test. \ No newline at end of file diff --git a/tests/federation/transport/server/test__base.py b/tests/federation/transport/server/test__base.py index 00a9c2064c6..447d0a071eb 100644 --- a/tests/federation/transport/server/test__base.py +++ b/tests/federation/transport/server/test__base.py @@ -34,7 +34,7 @@ from synapse.util.ratelimitutils import FederationRateLimiter from tests import unittest -from tests.http.server._base import test_disconnect +from tests.http.server._base import disconnect_and_assert class CancellableFederationServlet(BaseFederationServlet): @@ -94,7 +94,7 @@ def test_cancellable_disconnect(self) -> None: # request won't be processed. self.pump() - test_disconnect( + disconnect_and_assert( self.reactor, channel, expect_cancellation=True, @@ -114,7 +114,7 @@ def test_uncancellable_disconnect(self) -> None: # request won't be processed. self.pump() - test_disconnect( + disconnect_and_assert( self.reactor, channel, expect_cancellation=False, diff --git a/tests/http/server/_base.py b/tests/http/server/_base.py index afa69d1b7b6..41dcc0e095e 100644 --- a/tests/http/server/_base.py +++ b/tests/http/server/_base.py @@ -59,7 +59,7 @@ T = TypeVar("T") -def test_disconnect( +def disconnect_and_assert( reactor: MemoryReactorClock, channel: FakeChannel, expect_cancellation: bool, diff --git a/tests/http/test_servlet.py b/tests/http/test_servlet.py index 2f1c8f03c6c..389d3f2e6e3 100644 --- a/tests/http/test_servlet.py +++ b/tests/http/test_servlet.py @@ -37,7 +37,7 @@ from synapse.util.duration import Duration from tests import unittest -from tests.http.server._base import test_disconnect +from tests.http.server._base import disconnect_and_assert def make_request(content: bytes | JsonDict) -> Mock: @@ -127,7 +127,7 @@ class TestRestServletCancellation(unittest.HomeserverTestCase): def test_cancellable_disconnect(self) -> None: """Test that handlers with the `@cancellable` flag can be cancelled.""" channel = self.make_request("GET", "/sleep", await_result=False) - test_disconnect( + disconnect_and_assert( self.reactor, channel, expect_cancellation=True, @@ -137,7 +137,7 @@ def test_cancellable_disconnect(self) -> None: def test_uncancellable_disconnect(self) -> None: """Test that handlers without the `@cancellable` flag cannot be cancelled.""" channel = self.make_request("POST", "/sleep", await_result=False) - test_disconnect( + disconnect_and_assert( self.reactor, channel, expect_cancellation=False, diff --git a/tests/replication/http/test__base.py b/tests/replication/http/test__base.py index 1c7e7e997bb..58ea542befb 100644 --- a/tests/replication/http/test__base.py +++ b/tests/replication/http/test__base.py @@ -33,7 +33,7 @@ from synapse.util.duration import Duration from tests import unittest -from tests.http.server._base import test_disconnect +from tests.http.server._base import disconnect_and_assert class CancellableReplicationEndpoint(ReplicationEndpoint): @@ -94,7 +94,7 @@ def test_cancellable_disconnect(self) -> None: """Test that handlers with the `@cancellable` flag can be cancelled.""" path = f"{REPLICATION_PREFIX}/{CancellableReplicationEndpoint.NAME}/" channel = self.make_request("POST", path, await_result=False, content={}) - test_disconnect( + disconnect_and_assert( self.reactor, channel, expect_cancellation=True, @@ -105,7 +105,7 @@ def test_uncancellable_disconnect(self) -> None: """Test that handlers without the `@cancellable` flag cannot be cancelled.""" path = f"{REPLICATION_PREFIX}/{UncancellableReplicationEndpoint.NAME}/" channel = self.make_request("POST", path, await_result=False, content={}) - test_disconnect( + disconnect_and_assert( self.reactor, channel, expect_cancellation=False, diff --git a/tests/test_server.py b/tests/test_server.py index 2a36dd4b304..2326fafc75e 100644 --- a/tests/test_server.py +++ b/tests/test_server.py @@ -41,7 +41,7 @@ from synapse.util.duration import Duration from tests import unittest -from tests.http.server._base import test_disconnect +from tests.http.server._base import disconnect_and_assert from tests.server import ( FakeChannel, FakeSite, @@ -506,7 +506,7 @@ def test_cancellable_disconnect(self) -> None: channel = make_request( self.reactor, self.site, "GET", "/sleep", await_result=False ) - test_disconnect( + disconnect_and_assert( self.reactor, channel, expect_cancellation=True, @@ -518,7 +518,7 @@ def test_uncancellable_disconnect(self) -> None: channel = make_request( self.reactor, self.site, "POST", "/sleep", await_result=False ) - test_disconnect( + disconnect_and_assert( self.reactor, channel, expect_cancellation=False, @@ -540,7 +540,7 @@ def test_cancellable_disconnect(self) -> None: channel = make_request( self.reactor, self.site, "GET", "/sleep", await_result=False ) - test_disconnect( + disconnect_and_assert( self.reactor, channel, expect_cancellation=True, @@ -552,6 +552,6 @@ def test_uncancellable_disconnect(self) -> None: channel = make_request( self.reactor, self.site, "POST", "/sleep", await_result=False ) - test_disconnect( + disconnect_and_assert( self.reactor, channel, expect_cancellation=False, expected_body=b"ok" ) From 16245f055096e8d0ab33b62e5769ade4b1a375c3 Mon Sep 17 00:00:00 2001 From: Olivier 'reivilibre Date: Fri, 20 Feb 2026 15:52:29 +0000 Subject: [PATCH 034/121] Fix the 'Login as a user' Admin API not checking if the user exists before issuing an access token. (#18518) Fixes: #18503 --------- Signed-off-by: Olivier 'reivilibre Co-authored-by: Quentin Gliech --- changelog.d/18518.bugfix | 1 + synapse/rest/admin/users.py | 7 +++++++ tests/rest/admin/test_user.py | 11 +++++++++++ 3 files changed, 19 insertions(+) create mode 100644 changelog.d/18518.bugfix diff --git a/changelog.d/18518.bugfix b/changelog.d/18518.bugfix new file mode 100644 index 00000000000..959528d7c84 --- /dev/null +++ b/changelog.d/18518.bugfix @@ -0,0 +1 @@ +Fix the 'Login as a user' Admin API not checking if the user exists before issuing an access token. \ No newline at end of file diff --git a/synapse/rest/admin/users.py b/synapse/rest/admin/users.py index ccd34d17d80..807a9cad5b2 100644 --- a/synapse/rest/admin/users.py +++ b/synapse/rest/admin/users.py @@ -1144,6 +1144,7 @@ def __init__(self, hs: "HomeServer"): self.store = hs.get_datastores().main self.auth = hs.get_auth() self.auth_handler = hs.get_auth_handler() + self.admin_handler = hs.get_admin_handler() self.is_mine_id = hs.is_mine_id async def on_POST( @@ -1158,6 +1159,12 @@ async def on_POST( HTTPStatus.BAD_REQUEST, "Only local users can be logged in as" ) + # Validate user_id + UserID.from_string(user_id) + _user_info_dict = await self.store.get_user_by_id(user_id) + if not _user_info_dict: + raise NotFoundError("User not found") + body = parse_json_object_from_request(request, allow_empty_body=True) valid_until_ms = body.get("valid_until_ms") diff --git a/tests/rest/admin/test_user.py b/tests/rest/admin/test_user.py index 6d0584fa63e..72937df9a63 100644 --- a/tests/rest/admin/test_user.py +++ b/tests/rest/admin/test_user.py @@ -4288,6 +4288,17 @@ def test_not_admin(self) -> None: self.assertEqual(403, channel.code, msg=channel.json_body) + def test_no_user(self) -> None: + """Try to log in as a user that doesn't exist.""" + channel = self.make_request( + "POST", + "/_synapse/admin/v1/users/%s/login" % urllib.parse.quote("@ghost:test"), + b"{}", + access_token=self.admin_user_tok, + ) + self.assertEqual(404, channel.code, msg=channel.json_body) + self.assertEqual(Codes.NOT_FOUND, channel.json_body["errcode"]) + def test_send_event(self) -> None: """Test that sending event as a user works.""" # Create a room. From b30607ccafcb7ba9ee3ee7b7b1c89128aecb38b6 Mon Sep 17 00:00:00 2001 From: Quentin Gliech Date: Tue, 24 Feb 2026 12:18:37 +0100 Subject: [PATCH 035/121] 1.148.0 --- CHANGES.md | 7 +++++++ debian/changelog | 6 ++++++ pyproject.toml | 2 +- 3 files changed, 14 insertions(+), 1 deletion(-) diff --git a/CHANGES.md b/CHANGES.md index e52ca3ed30f..cae604adac6 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,3 +1,10 @@ +# Synapse 1.148.0 (2026-02-24) + +No significant changes since 1.148.0rc1. + + + + # Synapse 1.148.0rc1 (2026-02-17) ## Features diff --git a/debian/changelog b/debian/changelog index 105ea2fcec1..31fd948be1e 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,3 +1,9 @@ +matrix-synapse-py3 (1.148.0) stable; urgency=medium + + * New synapse release 1.148.0. + + -- Synapse Packaging team Tue, 24 Feb 2026 11:17:49 +0000 + matrix-synapse-py3 (1.148.0~rc1) stable; urgency=medium * New synapse release 1.148.0rc1. diff --git a/pyproject.toml b/pyproject.toml index 07455c57df5..0b8dff90589 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "matrix-synapse" -version = "1.148.0rc1" +version = "1.148.0" description = "Homeserver for the Matrix decentralised comms protocol" readme = "README.rst" authors = [ From bc15ed3c62c37a8904af24ab7af7f4438d5d48b6 Mon Sep 17 00:00:00 2001 From: Brad Murray Date: Tue, 24 Feb 2026 15:18:52 -0500 Subject: [PATCH 036/121] DeviceHandler: Add a log line when we delete a device (#19496) Deleting devices should be fairly rare, and if someone gets logged out it's helpful to grep logs for a user id or device id and see where it died. --- changelog.d/19496.misc | 1 + synapse/handlers/device.py | 2 ++ 2 files changed, 3 insertions(+) create mode 100644 changelog.d/19496.misc diff --git a/changelog.d/19496.misc b/changelog.d/19496.misc new file mode 100644 index 00000000000..83c5d6987c0 --- /dev/null +++ b/changelog.d/19496.misc @@ -0,0 +1 @@ +Add a log line when we delete devices. Contributed by @bradtgmurray @ Beeper. diff --git a/synapse/handlers/device.py b/synapse/handlers/device.py index 4552667176f..29074f5e20c 100644 --- a/synapse/handlers/device.py +++ b/synapse/handlers/device.py @@ -290,6 +290,8 @@ async def delete_devices(self, user_id: str, device_ids: StrCollection) -> None: user_id: The user to delete devices from. device_ids: The list of device IDs to delete """ + logger.info("Deleting devices %r for %r", list(device_ids), user_id) + to_device_stream_id = self._event_sources.get_current_token().to_device_key try: From ac3a1155113af8896ecf3f9fcfdf5cfe23b3e90c Mon Sep 17 00:00:00 2001 From: Eric Eastwood Date: Wed, 25 Feb 2026 09:47:13 -0600 Subject: [PATCH 037/121] Log if we ever `gc.freeze()` (#19440) Spawning from https://github.com/element-hq/synapse-small-hosts/issues/348 where some test appears to be flaky because some homeserver objects are frozen in the garbage collector. We set [`freeze=False`](https://github.com/element-hq/synapse-small-hosts/blob/a9a6869aa9a67176bdddc3b8ae2d0de0996d8cf4/multi_synapse/app/shard.py#L319-L321) in the [Synapse Pro for small hosts](https://docs.element.io/latest/element-server-suite-pro/synapse-pro-for-small-hosts/overview/) code but I just want to use this log to make extra sure this isn't being run somehow. The follow-up here would be to see what else would cause something to be frozen in the garbage collector. --- changelog.d/19440.misc | 1 + synapse/app/_base.py | 5 +++++ 2 files changed, 6 insertions(+) create mode 100644 changelog.d/19440.misc diff --git a/changelog.d/19440.misc b/changelog.d/19440.misc new file mode 100644 index 00000000000..77776011144 --- /dev/null +++ b/changelog.d/19440.misc @@ -0,0 +1 @@ +Add log to explain when and why we freeze objects in the garbage collector. diff --git a/synapse/app/_base.py b/synapse/app/_base.py index c64c41e9d27..7f4855f36bd 100644 --- a/synapse/app/_base.py +++ b/synapse/app/_base.py @@ -776,6 +776,11 @@ def log_shutdown() -> None: # # PyPy does not (yet?) implement gc.freeze() if hasattr(gc, "freeze"): + logger.info( + "garbage collector: Freezing all allocated objects in the hopes that (almost) " + "everything currently allocated are things that will be used by the homeserver " + "for the rest of time. Doing so means less work each GC (hopefully)." + ) gc.collect() gc.freeze() From f78d011df11040987b1215f42942330c03b09502 Mon Sep 17 00:00:00 2001 From: Hugh Nimmo-Smith Date: Wed, 25 Feb 2026 17:41:51 +0000 Subject: [PATCH 038/121] Experimental implementation of unstable MSC4388 for Sign in with QR (#19127) Co-authored-by: Olivier 'reivilibre' --- changelog.d/19127.feature | 1 + rust/src/lib.rs | 2 + rust/src/msc4388_rendezvous/mod.rs | 370 +++++++++ rust/src/msc4388_rendezvous/session.rs | 153 ++++ synapse/config/experimental.py | 23 +- synapse/rest/client/rendezvous.py | 50 +- synapse/rest/client/versions.py | 4 +- synapse/server.py | 5 + synapse/synapse_rust/msc4388_rendezvous.pyi | 33 + tests/rest/client/test_msc4388_rendezvous.py | 743 +++++++++++++++++++ 10 files changed, 1381 insertions(+), 3 deletions(-) create mode 100644 changelog.d/19127.feature create mode 100644 rust/src/msc4388_rendezvous/mod.rs create mode 100644 rust/src/msc4388_rendezvous/session.rs create mode 100644 synapse/synapse_rust/msc4388_rendezvous.pyi create mode 100644 tests/rest/client/test_msc4388_rendezvous.py diff --git a/changelog.d/19127.feature b/changelog.d/19127.feature new file mode 100644 index 00000000000..7dc3a49f368 --- /dev/null +++ b/changelog.d/19127.feature @@ -0,0 +1 @@ +Add experimental support for [MSC4388: Secure out-of-band channel for sign in with QR](https://github.com/matrix-org/matrix-spec-proposals/pull/4388). diff --git a/rust/src/lib.rs b/rust/src/lib.rs index fe880af2eae..83b8de7b64a 100644 --- a/rust/src/lib.rs +++ b/rust/src/lib.rs @@ -12,6 +12,7 @@ pub mod http; pub mod http_client; pub mod identifier; pub mod matrix_const; +pub mod msc4388_rendezvous; pub mod push; pub mod rendezvous; pub mod segmenter; @@ -55,6 +56,7 @@ fn synapse_rust(py: Python<'_>, m: &Bound<'_, PyModule>) -> PyResult<()> { events::register_module(py, m)?; http_client::register_module(py, m)?; rendezvous::register_module(py, m)?; + msc4388_rendezvous::register_module(py, m)?; segmenter::register_module(py, m)?; Ok(()) diff --git a/rust/src/msc4388_rendezvous/mod.rs b/rust/src/msc4388_rendezvous/mod.rs new file mode 100644 index 00000000000..bc9463639fd --- /dev/null +++ b/rust/src/msc4388_rendezvous/mod.rs @@ -0,0 +1,370 @@ +/* + * This file is licensed under the Affero General Public License (AGPL) version 3. + * + * Copyright (C) 2026 Element Creations Ltd + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * See the GNU Affero General Public License for more details: + * . + */ + +use std::{ + collections::BTreeMap, + time::{Duration, SystemTime}, +}; + +use http::StatusCode; +use pyo3::{ + pyclass, pymethods, + types::{PyAnyMethods, PyModule, PyModuleMethods}, + Bound, IntoPyObject, Py, PyAny, PyResult, Python, +}; +use serde::Deserialize; +use ulid::Ulid; + +use self::session::Session; +use crate::{ + duration::SynapseDuration, + errors::{NotFoundError, SynapseError}, + http::http_request_from_twisted, + msc4388_rendezvous::session::{GetResponse, PostResponse, PutResponse}, + UnwrapInfallible, +}; + +mod session; + +#[pyclass] +struct MSC4388RendezvousHandler { + clock: Py, + sessions: BTreeMap, + soft_limit: usize, + hard_limit: usize, + max_content_length: u64, + ttl: Duration, +} + +impl MSC4388RendezvousHandler { + /// Check the length of the data parameter and throw error if invalid. + fn check_data_length(&self, data: &str) -> PyResult<()> { + let data_length = data.len() as u64; + if data_length > self.max_content_length { + return Err(SynapseError::new( + StatusCode::PAYLOAD_TOO_LARGE, + "Payload too large".to_owned(), + "M_TOO_LARGE", + None, + None, + )); + } + Ok(()) + } + + /// Evict expired sessions and remove the oldest sessions until we're under the capacity. + fn evict(&mut self, now: SystemTime) { + // First remove all the entries which expired + self.sessions.retain(|_, session| !session.expired(now)); + + // Then we remove the oldest entries until we're under the soft limit + while self.sessions.len() > self.soft_limit { + self.sessions.pop_first(); + } + } +} + +#[derive(Deserialize)] +pub struct PostRequest { + data: String, +} + +#[derive(Deserialize)] +pub struct PutRequest { + sequence_token: String, + data: String, +} + +#[pymethods] +impl MSC4388RendezvousHandler { + #[new] + #[pyo3(signature = (homeserver, /, soft_limit=100, hard_limit=200,max_content_length=4*1024, eviction_interval=60*1000, ttl=2*60*1000))] + fn new( + py: Python<'_>, + homeserver: &Bound<'_, PyAny>, + soft_limit: usize, + hard_limit: usize, + max_content_length: u64, + eviction_interval: u64, + ttl: u64, + ) -> PyResult> { + let clock = homeserver + .call_method0("get_clock")? + .into_pyobject(py) + .unwrap_infallible() + .unbind(); + + // Construct a Python object so that we can get a reference to the + // evict method and schedule it to run. + let self_ = Py::new( + py, + Self { + clock, + sessions: BTreeMap::new(), + soft_limit, + hard_limit, + max_content_length, + ttl: Duration::from_millis(ttl), + }, + )?; + + let eviction_duration = SynapseDuration::from_milliseconds(eviction_interval); + + let evict = self_.getattr(py, "_evict")?; + homeserver.call_method0("get_clock")?.call_method( + "looping_call", + (evict, &eviction_duration), + None, + )?; + + Ok(self_) + } + + fn _evict(&mut self, py: Python<'_>) -> PyResult<()> { + let clock = self.clock.bind(py); + let now: u64 = clock.call_method0("time_msec")?.extract()?; + let now = SystemTime::UNIX_EPOCH + Duration::from_millis(now); + self.evict(now); + + Ok(()) + } + + fn handle_post( + &mut self, + py: Python<'_>, + twisted_request: &Bound<'_, PyAny>, + ) -> PyResult<(u8, PostResponse)> { + let clock = self.clock.bind(py); + let now: u64 = clock.call_method0("time_msec")?.extract()?; + let now = SystemTime::UNIX_EPOCH + Duration::from_millis(now); + + // We trigger an immediate eviction if we're at the hard limit + if self.sessions.len() >= self.hard_limit { + self.evict(now); + } + + // Generate a new ULID for the session from the current time. + let id = Ulid::from_datetime(now); + + let request = http_request_from_twisted(twisted_request)?; + // parse JSON body + let post_request: PostRequest = + serde_json::from_slice(&request.into_body()).map_err(|_| { + SynapseError::new( + StatusCode::BAD_REQUEST, + "Invalid JSON in request body".to_owned(), + "M_INVALID_PARAM", + None, + None, + ) + })?; + + let data: String = post_request.data; + self.check_data_length(&data)?; + + let session = Session::new(id, data, now, self.ttl); + let response = session.post_response(now); + self.sessions.insert(id, session); + + Ok((200, response)) + } + + fn handle_get( + &mut self, + py: Python<'_>, + id: &str, + twisted_request: &Bound<'_, PyAny>, + ) -> PyResult<(u8, GetResponse)> { + let request = http_request_from_twisted(twisted_request)?; + + // As per the MSC, we check the Sec-Fetch-* headers to ensure this request did not come from somewhere that will + // be rendered directly to the user, as the response may contain sensitive data. These headers are added by + // well behaved browsers so are helpful for protecting regular users. + + // Sec-Fetch-Dest: https://www.w3.org/TR/fetch-metadata/#sec-fetch-dest-header + // + // If the header is present then this must be "empty". All other values such as document, image etc. + // are considered potentially dangerous as they might be rendered to the user. + // + // Note that because we only ever return JSON, so it is unlikely that it could somehow be rendered as an image, + // video or other media. + let sec_fetch_dest: Option = request + .headers() + .get("sec-fetch-dest") + .and_then(|v| v.to_str().ok()) + .map(|s| s.to_owned()); + if sec_fetch_dest.is_some() && sec_fetch_dest.as_deref() != Some("empty") { + return Err(SynapseError::new( + StatusCode::FORBIDDEN, + "Rendezvous content is not accessible from the request destination".to_owned(), + "M_FORBIDDEN", + None, + None, + )); + } + + // Sec-Fetch-Mode: https://www.w3.org/TR/fetch-metadata/#sec-fetch-mode-header + // + // A request mode of "navigate" is not allowed as this indicates the request is being made by the + // browser to navigate to a URL, which could lead to the response being rendered directly to the user. + // + // Note that usually Sec-Fetch-Dest would be "document" in this case and so the request would be rejected earlier, + // but we check the mode just in case the destination is not set correctly. + let sec_fetch_mode: Option = request + .headers() + .get("sec-fetch-mode") + .and_then(|v| v.to_str().ok()) + .map(|s| s.to_owned()); + if sec_fetch_mode.as_deref() == Some("navigate") { + return Err(SynapseError::new( + StatusCode::FORBIDDEN, + "Rendezvous content is not accessible via top-level navigation".to_owned(), + "M_FORBIDDEN", + None, + None, + )); + } + + // Sec-Fetch-User: https://www.w3.org/TR/fetch-metadata/#sec-fetch-user-header + // + // If the request has a Sec-Fetch-User header with a value of "?1", this indicates that the + // request was triggered by user activation, such as a click. + // + // Note that usually Sec-Fetch-Mode would be "navigate" or the Sec-Fetch-Dest would be "document" in this case + // and so the request would be rejected earlier, but we check the user activation just in case those headers are + // not set correctly. + let sec_fetch_user: Option = request + .headers() + .get("sec-fetch-user") + .and_then(|v| v.to_str().ok()) + .map(|s| s.to_owned()); + if sec_fetch_user.as_deref() == Some("?1") { + return Err(SynapseError::new( + StatusCode::FORBIDDEN, + "Rendezvous content is not accessible from requests with user activation" + .to_owned(), + "M_FORBIDDEN", + None, + None, + )); + } + + // Sec-Fetch-Site: https://www.w3.org/TR/fetch-metadata/#sec-fetch-site-header + // + // "none" indicates the request did not originate from a web page + // (e.g. typed URL, bookmark, or browser extension), so we disallow it. + let sec_fetch_site: Option = request + .headers() + .get("sec-fetch-site") + .and_then(|v| v.to_str().ok()) + .map(|s| s.to_owned()); + if sec_fetch_site.as_deref() == Some("none") { + return Err(SynapseError::new( + StatusCode::FORBIDDEN, + "Rendezvous content is not accessible from requests from user interaction" + .to_owned(), + "M_FORBIDDEN", + None, + None, + )); + } + + let clock = self.clock.bind(py); + let now: u64 = clock.call_method0("time_msec")?.extract()?; + let now = SystemTime::UNIX_EPOCH + Duration::from_millis(now); + + let id: Ulid = id.parse().map_err(|_| NotFoundError::new())?; + let session = self + .sessions + .get(&id) + .filter(|s| !s.expired(now)) + .ok_or_else(NotFoundError::new)?; + + Ok((200, session.get_response(now))) + } + + fn handle_put( + &mut self, + py: Python<'_>, + id: &str, + twisted_request: &Bound<'_, PyAny>, + ) -> PyResult<(u8, PutResponse)> { + let request = http_request_from_twisted(twisted_request)?; + // parse JSON body + let put_request: PutRequest = + serde_json::from_slice(&request.into_body()).map_err(|_| { + SynapseError::new( + StatusCode::BAD_REQUEST, + "Invalid JSON in request body".to_owned(), + "M_INVALID_PARAM", + None, + None, + ) + })?; + + let sequence_token: String = put_request.sequence_token; + + let data: String = put_request.data; + + self.check_data_length(&data)?; + + let clock = self.clock.bind(py); + let now: u64 = clock.call_method0("time_msec")?.extract()?; + let now = SystemTime::UNIX_EPOCH + Duration::from_millis(now); + + let id: Ulid = id.parse().map_err(|_| NotFoundError::new())?; + let session = self + .sessions + .get_mut(&id) + .filter(|s| !s.expired(now)) + .ok_or_else(NotFoundError::new)?; + + if !session.sequence_token().eq(&sequence_token) { + return Err(SynapseError::new( + StatusCode::CONFLICT, + "sequence_token does not match".to_owned(), + "IO_ELEMENT_MSC4388_CONCURRENT_WRITE", + None, + None, + )); + } + + session.update(data, now); + + Ok((200, session.put_response())) + } + + fn handle_delete(&mut self, id: &str) -> PyResult<(u8, ())> { + let id: Ulid = id.parse().map_err(|_| NotFoundError::new())?; + let _session = self.sessions.remove(&id).ok_or_else(NotFoundError::new)?; + + Ok((200, ())) + } +} + +pub fn register_module(py: Python<'_>, m: &Bound<'_, PyModule>) -> PyResult<()> { + let child_module = PyModule::new(py, "msc4388_rendezvous")?; + + child_module.add_class::()?; + + m.add_submodule(&child_module)?; + + // We need to manually add the module to sys.modules to make `from + // synapse.synapse_rust import rendezvous` work. + py.import("sys")? + .getattr("modules")? + .set_item("synapse.synapse_rust.msc4388_rendezvous", child_module)?; + + Ok(()) +} diff --git a/rust/src/msc4388_rendezvous/session.rs b/rust/src/msc4388_rendezvous/session.rs new file mode 100644 index 00000000000..467d1b5baf5 --- /dev/null +++ b/rust/src/msc4388_rendezvous/session.rs @@ -0,0 +1,153 @@ +/* + * This file is licensed under the Affero General Public License (AGPL) version 3. + * + * Copyright (C) 2026 Element Creations Ltd + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * See the GNU Affero General Public License for more details: + * . + */ + +use std::time::{Duration, SystemTime}; + +use base64::{engine::general_purpose::URL_SAFE_NO_PAD, Engine as _}; +use pyo3::{Bound, IntoPyObject, PyAny, Python}; +use pythonize::{pythonize, PythonizeError}; +use serde::Serialize; +use sha2::{Digest, Sha256}; +use ulid::Ulid; + +/// A single session, containing data, metadata, and expiry information. +pub struct Session { + id: Ulid, + hash: [u8; 32], + data: String, + last_modified: SystemTime, + expires: SystemTime, +} + +#[derive(Serialize)] +pub struct PostResponse { + id: String, + sequence_token: String, + expires_in_ms: u64, +} + +impl<'source> IntoPyObject<'source> for PostResponse { + type Target = PyAny; + type Output = Bound<'source, Self::Target>; + type Error = PythonizeError; + + fn into_pyobject(self, py: Python<'source>) -> Result { + pythonize(py, &self) + } +} + +#[derive(Serialize)] +pub struct GetResponse { + data: String, + sequence_token: String, + expires_in_ms: u64, +} + +impl<'source> IntoPyObject<'source> for GetResponse { + type Target = PyAny; + type Output = Bound<'source, Self::Target>; + type Error = PythonizeError; + + fn into_pyobject(self, py: Python<'source>) -> Result { + pythonize(py, &self) + } +} + +#[derive(Serialize)] +pub struct PutResponse { + sequence_token: String, +} + +impl<'source> IntoPyObject<'source> for PutResponse { + type Target = PyAny; + type Output = Bound<'source, Self::Target>; + type Error = PythonizeError; + + fn into_pyobject(self, py: Python<'source>) -> Result { + pythonize(py, &self) + } +} + +impl Session { + /// Create a new session with the given data and time-to-live. + pub fn new(id: Ulid, data: String, now: SystemTime, ttl: Duration) -> Self { + let hash = Self::compute_hash(&data, now); + Self { + id, + hash, + data, + expires: now + ttl, + last_modified: now, + } + } + + /// Returns true if the session has expired at the given time. + pub fn expired(&self, now: SystemTime) -> bool { + self.expires <= now + } + + /// Update the session with new data and last modified time. + pub fn update(&mut self, data: String, now: SystemTime) { + self.hash = Self::compute_hash(&data, now); + self.data = data; + self.last_modified = now; + } + + /// Compute the hash of the data and timestamp. + fn compute_hash(data: &str, now: SystemTime) -> [u8; 32] { + let mut hasher = Sha256::new(); + hasher.update(data); + let now_millis = now + .duration_since(SystemTime::UNIX_EPOCH) + .unwrap_or_default() + .as_millis(); + hasher.update(now_millis.to_be_bytes()); + hasher.finalize().into() + } + + /// The sequence token for the session. + pub fn sequence_token(&self) -> String { + URL_SAFE_NO_PAD.encode(self.hash) + } + + pub fn get_response(&self, now: SystemTime) -> GetResponse { + GetResponse { + data: self.data.clone(), + sequence_token: self.sequence_token(), + expires_in_ms: self + .expires + .duration_since(now) + .unwrap_or_default() + .as_millis() as u64, + } + } + + pub fn post_response(&self, now: SystemTime) -> PostResponse { + PostResponse { + id: self.id.to_string(), + sequence_token: self.sequence_token(), + expires_in_ms: self + .expires + .duration_since(now) + .unwrap_or_default() + .as_millis() as u64, + } + } + + pub fn put_response(&self) -> PutResponse { + PutResponse { + sequence_token: self.sequence_token(), + } + } +} diff --git a/synapse/config/experimental.py b/synapse/config/experimental.py index b6c8b8c0625..05091ca6eb6 100644 --- a/synapse/config/experimental.py +++ b/synapse/config/experimental.py @@ -509,7 +509,8 @@ def read_config( "msc4069_profile_inhibit_propagation", False ) - # MSC4108: Mechanism to allow OIDC sign in and E2EE set up via QR code + # MSC4108: Mechanism to allow OIDC sign in and E2EE set up via QR code - 2024 version: + # See: https://github.com/element-hq/synapse/issues/19434 self.msc4108_enabled = experimental.get("msc4108_enabled", False) self.msc4108_delegation_endpoint: str | None = experimental.get( @@ -534,6 +535,26 @@ def read_config( ("experimental", "msc4108_delegation_endpoint"), ) + # MSC4388: Secure out-of-band channel for sign in with QR: + # See: https://github.com/element-hq/synapse/issues/19433 + msc4388_mode = experimental.get("msc4388_mode", "off") + + if msc4388_mode not in ["off", "public", "authenticated"]: + raise ConfigError( + "msc4388_mode must be one of 'off', 'public' or 'authenticated'", + ("experimental", "msc4388_mode"), + ) + self.msc4388_enabled: bool = msc4388_mode != "off" + self.msc4388_requires_authentication: bool = msc4388_mode == "authenticated" + + if self.msc4388_enabled and not ( + config.get("matrix_authentication_service") or {} + ).get("enabled", False): + raise ConfigError( + "MSC4388 requires matrix_authentication_service to be enabled", + ("experimental", "msc4388_enabled"), + ) + # MSC4133: Custom profile fields self.msc4133_enabled: bool = experimental.get("msc4133_enabled", False) diff --git a/synapse/rest/client/rendezvous.py b/synapse/rest/client/rendezvous.py index 08a449eefc7..bd9205fc5f0 100644 --- a/synapse/rest/client/rendezvous.py +++ b/synapse/rest/client/rendezvous.py @@ -21,7 +21,7 @@ import logging from http.client import TEMPORARY_REDIRECT -from typing import TYPE_CHECKING +from typing import TYPE_CHECKING, Any from synapse.http.server import HttpServer, respond_with_redirect from synapse.http.servlet import RestServlet @@ -68,9 +68,57 @@ def on_POST(self, request: SynapseRequest) -> None: self._handler.handle_post(request) +class MSC4388CreateRendezvousServlet(RestServlet): + PATTERNS = client_patterns( + "/io.element.msc4388/rendezvous$", releases=[], v1=False, unstable=True + ) + + def __init__(self, hs: "HomeServer") -> None: + super().__init__() + self._handler = hs.get_msc4388_rendezvous_handler() + self.auth = hs.get_auth() + self.require_authentication = ( + hs.config.experimental.msc4388_requires_authentication + ) + + async def on_POST(self, request: SynapseRequest) -> tuple[int, Any]: + if self.require_authentication: + # This will raise if the user is not authenticated + await self.auth.get_user_by_req(request) + return self._handler.handle_post(request) + + +class MSC4388UpdateRendezvousServlet(RestServlet): + PATTERNS = client_patterns( + "/io.element.msc4388/rendezvous/(?P[^/]+)$", + releases=[], + v1=False, + unstable=True, + ) + + def __init__(self, hs: "HomeServer") -> None: + super().__init__() + self._handler = hs.get_msc4388_rendezvous_handler() + + def on_GET(self, request: SynapseRequest, rendezvous_id: str) -> tuple[int, Any]: + return self._handler.handle_get(rendezvous_id, request) + + def on_PUT(self, request: SynapseRequest, rendezvous_id: str) -> tuple[int, Any]: + return self._handler.handle_put(rendezvous_id, request) + + def on_DELETE( + self, _request: SynapseRequest, rendezvous_id: str + ) -> tuple[int, Any]: + return self._handler.handle_delete(rendezvous_id) + + def register_servlets(hs: "HomeServer", http_server: HttpServer) -> None: if hs.config.experimental.msc4108_enabled: MSC4108RendezvousServlet(hs).register(http_server) if hs.config.experimental.msc4108_delegation_endpoint is not None: MSC4108DelegationRendezvousServlet(hs).register(http_server) + + if hs.config.experimental.msc4388_enabled: + MSC4388CreateRendezvousServlet(hs).register(http_server) + MSC4388UpdateRendezvousServlet(hs).register(http_server) diff --git a/synapse/rest/client/versions.py b/synapse/rest/client/versions.py index 89458495311..e443629faba 100644 --- a/synapse/rest/client/versions.py +++ b/synapse/rest/client/versions.py @@ -161,7 +161,7 @@ async def on_GET(self, request: SynapseRequest) -> tuple[int, JsonDict]: "org.matrix.msc4069": self.config.experimental.msc4069_profile_inhibit_propagation, # Allows clients to handle push for encrypted events. "org.matrix.msc4028": self.config.experimental.msc4028_push_encrypted_events, - # MSC4108: Mechanism to allow OIDC sign in and E2EE set up via QR code + # MSC4108: Mechanism to allow OIDC sign in and E2EE set up via QR code - 2024 version "org.matrix.msc4108": ( self.config.experimental.msc4108_enabled or ( @@ -169,6 +169,8 @@ async def on_GET(self, request: SynapseRequest) -> tuple[int, JsonDict]: is not None ) ), + # MSC4388: Secure out-of-band channel for sign in with QR + "io.element.msc4388": (self.config.experimental.msc4388_enabled), # MSC4140: Delayed events "org.matrix.msc4140": bool(self.config.server.max_event_delay_ms), # Simplified sliding sync diff --git a/synapse/server.py b/synapse/server.py index e6337c379b2..8bf19f11b5d 100644 --- a/synapse/server.py +++ b/synapse/server.py @@ -174,6 +174,7 @@ from synapse.storage import Databases from synapse.storage.controllers import StorageControllers from synapse.streams.events import EventSources +from synapse.synapse_rust.msc4388_rendezvous import MSC4388RendezvousHandler from synapse.synapse_rust.rendezvous import RendezvousHandler from synapse.types import DomainSpecificString, ISynapseReactor from synapse.util import SYNAPSE_VERSION @@ -1184,6 +1185,10 @@ def get_room_forgetter_handler(self) -> RoomForgetterHandler: def get_rendezvous_handler(self) -> RendezvousHandler: return RendezvousHandler(self) + @cache_in_self + def get_msc4388_rendezvous_handler(self) -> MSC4388RendezvousHandler: + return MSC4388RendezvousHandler(self) + @cache_in_self def get_outbound_redis_connection(self) -> "ConnectionHandler": """ diff --git a/synapse/synapse_rust/msc4388_rendezvous.pyi b/synapse/synapse_rust/msc4388_rendezvous.pyi new file mode 100644 index 00000000000..f8e064ef64f --- /dev/null +++ b/synapse/synapse_rust/msc4388_rendezvous.pyi @@ -0,0 +1,33 @@ +# This file is licensed under the Affero General Public License (AGPL) version 3. +# +# Copyright (C) 2026 Element Creations Ltd +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# See the GNU Affero General Public License for more details: +# . + +from typing import Any + +from twisted.web.iweb import IRequest + +from synapse.server import HomeServer + +class MSC4388RendezvousHandler: + def __init__( + self, + homeserver: HomeServer, + /, + soft_limit: int = 100, # On each background eviction run sessions will be removed until we're under this limit + hard_limit: int = 200, # If this limit is reached an immediate eviction will be triggered + max_content_length: int = 4 * 1024, # MSC4388 specifies maximum of 4KB + eviction_interval: int = 60 * 1000, + ttl: int = 2 * 60 * 1000, # MSC4388 specifies minimum of 120 seconds + ) -> None: ... + def handle_post(self, request: IRequest) -> tuple[int, Any]: ... + def handle_get(self, session_id: str, request: IRequest) -> tuple[int, Any]: ... + def handle_put(self, session_id: str, request: IRequest) -> tuple[int, Any]: ... + def handle_delete(self, session_id: str) -> tuple[int, Any]: ... diff --git a/tests/rest/client/test_msc4388_rendezvous.py b/tests/rest/client/test_msc4388_rendezvous.py new file mode 100644 index 00000000000..f16bb3f344a --- /dev/null +++ b/tests/rest/client/test_msc4388_rendezvous.py @@ -0,0 +1,743 @@ +# +# This file is licensed under the Affero General Public License (AGPL) version 3. +# +# Copyright (C) 2026 Element Creations Ltd +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# See the GNU Affero General Public License for more details: +# . + + +import json +import urllib.parse +from typing import Any, Mapping +from unittest.mock import Mock + +from parameterized import parameterized + +from twisted.internet.testing import MemoryReactor + +from synapse.api.auth.mas import MasDelegatedAuth +from synapse.rest import admin +from synapse.rest.client import login, rendezvous +from synapse.server import HomeServer +from synapse.types import UserID +from synapse.util.clock import Clock + +from tests import unittest +from tests.unittest import checked_cast, override_config + +rz_endpoint = "/_matrix/client/unstable/io.element.msc4388/rendezvous" + + +class RendezvousServletTestCase(unittest.HomeserverTestCase): + """ + Test the experimental MSC4388 rendezvous endpoint. + """ + + servlets = [ + admin.register_servlets, + login.register_servlets, + rendezvous.register_servlets, + ] + + def make_homeserver(self, reactor: MemoryReactor, clock: Clock) -> HomeServer: + self.hs = self.setup_test_homeserver() + return self.hs + + def setup_mock_oauth(self) -> None: + """ + This isn't a very elegant way to mock the OAuth API, but it works for our purposes. + """ + + self.auth = checked_cast(MasDelegatedAuth, self.hs.get_auth()) + + self._rust_client = Mock(spec=["post"]) + self._rust_client.post = self._mock_oauth_response + self.auth._rust_http_client = self._rust_client + + async def _mock_oauth_response( + self, + url: str, + response_limit: int, + headers: Mapping[str, str], + request_body: str, + ) -> bytes: + # get the token from the request body which is form encoded + parsed_body = urllib.parse.parse_qs(request_body) + token = parsed_body.get("token", [""])[0] + + if not token.startswith("mock_token_"): + return bytes(json.dumps({"active": False}).encode("utf-8")) + token = token.replace("mock_token_", "") + + username, device_id = token.split("_", 1) + user_id = UserID(username, self.hs.hostname) + store = self.hs.get_datastores().main + + # Check th user exists in the store + user_info = await store.get_user_by_id(user_id=user_id.to_string()) + if user_info is None: + return bytes(json.dumps({"active": False}).encode("utf-8")) + + # Check the device exists in the store + device = await store.get_device( + user_id=user_id.to_string(), device_id=device_id + ) + if device is None: + return bytes(json.dumps({"active": False}).encode("utf-8")) + + return bytes( + json.dumps( + { + "active": True, + "scope": "urn:matrix:client:device:" + + device_id + + " urn:matrix:client:api:*", + "username": username, + } + ).encode("utf-8") + ) + + def register_oauth_user(self, username: str, device_id: str) -> str: + # Provision the user and the device + store = self.hs.get_datastores().main + user_id = UserID(username, self.hs.hostname) + + self.get_success(store.register_user(user_id=user_id.to_string())) + self.get_success( + store.store_device( + user_id=user_id.to_string(), + device_id=device_id, + initial_device_display_name=None, + ) + ) + # Generate an access token for the device + return "mock_token_" + username + "_" + device_id + + def test_disabled(self) -> None: + channel = self.make_request("POST", rz_endpoint, {}, access_token=None) + self.assertEqual(channel.code, 404) + + @override_config( + { + "experimental_features": { + "msc4388_mode": "off", + }, + } + ) + def test_off(self) -> None: + channel = self.make_request("POST", rz_endpoint, {}, access_token=None) + self.assertEqual(channel.code, 404) + + @override_config( + { + "disable_registration": True, + "matrix_authentication_service": { + "enabled": True, + "secret": "secret_value", + "endpoint": "https://issuer", + }, + "experimental_features": { + "msc4388_mode": "public", + }, + } + ) + def test_rendezvous_public(self) -> None: + """ + Test the MSC4108 rendezvous endpoint, including: + - Creating a session + - Getting the data back + - Updating the data + - Deleting the data + - Sequence token handling + """ + # We can post arbitrary data to the endpoint + channel = self.make_request( + "POST", + rz_endpoint, + {"data": "foo=bar"}, + access_token=None, + ) + self.assertEqual(channel.code, 200) + rendezvous_id = channel.json_body["id"] + sequence_token = channel.json_body["sequence_token"] + expires_in_ms = channel.json_body["expires_in_ms"] + self.assertGreater(expires_in_ms, 0) + + session_endpoint = rz_endpoint + f"/{rendezvous_id}" + + # We can get the data back + # Advances clock by 100ms + channel = self.make_request( + "GET", + session_endpoint, + access_token=None, + ) + + self.assertEqual(channel.code, 200) + self.assertEqual(channel.json_body["data"], "foo=bar") + self.assertEqual(channel.json_body["sequence_token"], sequence_token) + self.assertEqual(channel.json_body["expires_in_ms"], expires_in_ms - 100) + + # We can update the data + # Advances clock by 100ms + channel = self.make_request( + "PUT", + session_endpoint, + {"sequence_token": sequence_token, "data": "foo=baz"}, + access_token=None, + ) + + self.assertEqual(channel.code, 200) + old_sequence_token = sequence_token + new_sequence_token = channel.json_body["sequence_token"] + + # If we try to update it again with the old etag, it should fail + # Advances clock by 100ms + channel = self.make_request( + "PUT", + session_endpoint, + {"sequence_token": old_sequence_token, "data": "bar=baz"}, + access_token=None, + ) + + self.assertEqual(channel.code, 409) + self.assertEqual( + channel.json_body["errcode"], "IO_ELEMENT_MSC4388_CONCURRENT_WRITE" + ) + + # We should get the updated data + # Advances clock by 100ms + channel = self.make_request( + "GET", + session_endpoint, + access_token=None, + ) + + self.assertEqual(channel.code, 200) + self.assertEqual(channel.json_body["data"], "foo=baz") + self.assertEqual(channel.json_body["sequence_token"], new_sequence_token) + self.assertEqual(channel.json_body["expires_in_ms"], expires_in_ms - 400) + + # We can delete the data + channel = self.make_request( + "DELETE", + session_endpoint, + access_token=None, + ) + + self.assertEqual(channel.code, 200) + + # If we try to get the data again, it should fail + channel = self.make_request( + "GET", + session_endpoint, + access_token=None, + ) + + self.assertEqual(channel.code, 404) + self.assertEqual(channel.json_body["errcode"], "M_NOT_FOUND") + + @override_config( + { + "disable_registration": True, + "matrix_authentication_service": { + "enabled": True, + "secret": "secret_value", + "endpoint": "https://issuer", + }, + "experimental_features": { + "msc4388_mode": "authenticated", + }, + } + ) + def test_rendezvous_requires_authentication(self) -> None: + """ + Test the MSC4108 rendezvous endpoint when configured with the mode authenticated, including: + - Creating a session + - Getting the data back + - Updating the data + - Deleting the data + - Sequence token handling + """ + self.setup_mock_oauth() + alice_token = self.register_oauth_user("alice", "device1") + + # This should fail without authentication: + channel = self.make_request( + "POST", + rz_endpoint, + {"data": "foo=bar"}, + access_token=None, + ) + self.assertEqual(channel.code, 401) + + # This should work as we are now authenticated + channel = self.make_request( + "POST", + rz_endpoint, + {"data": "foo=bar"}, + access_token=alice_token, + ) + self.assertEqual(channel.code, 200) + rendezvous_id = channel.json_body["id"] + sequence_token = channel.json_body["sequence_token"] + expires_in_ms = channel.json_body["expires_in_ms"] + self.assertGreater(expires_in_ms, 0) + + session_endpoint = rz_endpoint + f"/{rendezvous_id}" + + # We can get the data back without authentication + channel = self.make_request( + "GET", + session_endpoint, + access_token=None, + ) + + self.assertEqual(channel.code, 200) + self.assertEqual(channel.json_body["data"], "foo=bar") + self.assertEqual(channel.json_body["sequence_token"], sequence_token) + self.assertEqual(channel.json_body["expires_in_ms"], expires_in_ms) + + # We can update the data without authentication + channel = self.make_request( + "PUT", + session_endpoint, + {"sequence_token": sequence_token, "data": "foo=baz"}, + access_token=None, + ) + + self.assertEqual(channel.code, 200) + new_sequence_token = channel.json_body["sequence_token"] + + # We should get the updated data without authentication + channel = self.make_request( + "GET", + session_endpoint, + access_token=None, + ) + + self.assertEqual(channel.code, 200) + self.assertEqual(channel.json_body["data"], "foo=baz") + self.assertEqual(channel.json_body["sequence_token"], new_sequence_token) + self.assertEqual(channel.json_body["expires_in_ms"], expires_in_ms - 200) + + # We can delete the data without authentication + channel = self.make_request( + "DELETE", + session_endpoint, + access_token=None, + ) + + self.assertEqual(channel.code, 200) + + # If we try to get the data again, it should fail + channel = self.make_request( + "GET", + session_endpoint, + access_token=None, + ) + + self.assertEqual(channel.code, 404) + self.assertEqual(channel.json_body["errcode"], "M_NOT_FOUND") + + @override_config( + { + "disable_registration": True, + "matrix_authentication_service": { + "enabled": True, + "secret": "secret_value", + "endpoint": "https://issuer", + }, + "experimental_features": { + "msc4388_mode": "public", + }, + } + ) + def test_expiration(self) -> None: + """ + Test that entries are evicted after a TTL. + """ + # Start a new session + channel = self.make_request( + "POST", + rz_endpoint, + {"data": "foo=bar"}, + access_token=None, + ) + self.assertEqual(channel.code, 200) + session_endpoint = rz_endpoint + "/" + channel.json_body["id"] + + # Sanity check that we can get the data back + channel = self.make_request( + "GET", + session_endpoint, + access_token=None, + ) + self.assertEqual(channel.code, 200) + self.assertEqual(channel.json_body["data"], "foo=bar") + + # Advance the clock, TTL of entries is 2 minutes + self.reactor.advance(120) + + # Get the data back, it should be gone + channel = self.make_request( + "GET", + session_endpoint, + access_token=None, + ) + self.assertEqual(channel.code, 404) + + @override_config( + { + "disable_registration": True, + "matrix_authentication_service": { + "enabled": True, + "secret": "secret_value", + "endpoint": "https://issuer", + }, + "experimental_features": { + "msc4388_mode": "public", + }, + } + ) + def test_capacity(self) -> None: + """ + Test that the soft capacity limit is enforced on the rendezvous sessions, as old + entries are evicted at an interval when the limit is reached. + """ + # Start a new session + channel = self.make_request( + "POST", + rz_endpoint, + {"data": "foo=bar"}, + access_token=None, + ) + self.assertEqual(channel.code, 200) + session_endpoint = rz_endpoint + "/" + channel.json_body["id"] + + # Sanity check that we can get the data back + channel = self.make_request( + "GET", + session_endpoint, + access_token=None, + ) + self.assertEqual(channel.code, 200) + self.assertEqual(channel.json_body["data"], "foo=bar") + + # We advance the clock to make sure that this entry is the "lowest" in the session list + self.reactor.advance(1) + + # Start a lot of new sessions + for _ in range(100): + channel = self.make_request( + "POST", + rz_endpoint, + {"data": "foo=bar"}, + access_token=None, + ) + self.assertEqual(channel.code, 200) + + # Get the data back, it should still be there, as the eviction hasn't run yet + channel = self.make_request( + "GET", + session_endpoint, + access_token=None, + ) + + self.assertEqual(channel.code, 200) + + # Advance the clock, as it will trigger the eviction + self.reactor.advance(59) + + # Get the data back, it should be gone + channel = self.make_request( + "GET", + session_endpoint, + access_token=None, + ) + + self.assertEqual(channel.code, 404) + + @override_config( + { + "disable_registration": True, + "matrix_authentication_service": { + "enabled": True, + "secret": "secret_value", + "endpoint": "https://issuer", + }, + "experimental_features": { + "msc4388_mode": "public", + }, + } + ) + def test_hard_capacity(self) -> None: + """ + Test that the hard capacity limit is enforced on the rendezvous sessions, as old + entries are evicted immediately when the limit is reached. + """ + # Start a new session + channel = self.make_request( + "POST", + rz_endpoint, + {"data": "foo=bar"}, + access_token=None, + ) + self.assertEqual(channel.code, 200) + session_endpoint = rz_endpoint + "/" + channel.json_body["id"] + # We advance the clock to make sure that this entry is the "lowest" in the session list + self.reactor.advance(1) + + # Sanity check that we can get the data back + channel = self.make_request( + "GET", + session_endpoint, + access_token=None, + ) + self.assertEqual(channel.code, 200) + self.assertEqual(channel.json_body["data"], "foo=bar") + + # Start a lot of new sessions + for _ in range(200): + channel = self.make_request( + "POST", + rz_endpoint, + {"data": "foo=bar"}, + access_token=None, + ) + self.assertEqual(channel.code, 200) + + # Get the data back, it should already be gone as we hit the hard limit + channel = self.make_request( + "GET", + session_endpoint, + access_token=None, + ) + + self.assertEqual(channel.code, 404) + + @override_config( + { + "disable_registration": True, + "matrix_authentication_service": { + "enabled": True, + "secret": "secret_value", + "endpoint": "https://issuer", + }, + "experimental_features": { + "msc4388_mode": "public", + }, + } + ) + def test_data_type(self) -> None: + """ + Test that the data field is restricted to string. + """ + invalid_datas: list[Any] = [123214, ["asd"], {"asd": "asdsad"}, None] + + # We cannot post invalid non-string data field values to the endpoint + for invalid_data in invalid_datas: + channel = self.make_request( + "POST", + rz_endpoint, + {"data": invalid_data}, + access_token=None, + ) + self.assertEqual(channel.code, 400) + self.assertEqual(channel.json_body["errcode"], "M_INVALID_PARAM") + + # Make a valid request + channel = self.make_request( + "POST", + rz_endpoint, + {"data": "test"}, + access_token=None, + ) + self.assertEqual(channel.code, 200) + rendezvous_id = channel.json_body["id"] + sequence_token = channel.json_body["sequence_token"] + + session_endpoint = rz_endpoint + f"/{rendezvous_id}" + + # We can't update the data with invalid data + for invalid_data in invalid_datas: + channel = self.make_request( + "PUT", + session_endpoint, + {"sequence_token": sequence_token, "data": invalid_data}, + access_token=None, + ) + self.assertEqual(channel.code, 400) + self.assertEqual(channel.json_body["errcode"], "M_INVALID_PARAM") + + @override_config( + { + "disable_registration": True, + "matrix_authentication_service": { + "enabled": True, + "secret": "secret_value", + "endpoint": "https://issuer", + }, + "experimental_features": { + "msc4388_mode": "public", + }, + } + ) + def test_max_length(self) -> None: + """ + Test that the data max length is restricted. + """ + too_long_data = "a" * 5000 # MSC4108 specifies 4KB max length + + channel = self.make_request( + "POST", + rz_endpoint, + {"data": too_long_data}, + access_token=None, + ) + self.assertEqual(channel.code, 413) + self.assertEqual(channel.json_body["errcode"], "M_TOO_LARGE") + + # Make a valid request + channel = self.make_request( + "POST", + rz_endpoint, + {"data": "test"}, + access_token=None, + ) + self.assertEqual(channel.code, 200) + rendezvous_id = channel.json_body["id"] + sequence_token = channel.json_body["sequence_token"] + + session_endpoint = rz_endpoint + f"/{rendezvous_id}" + + # We can't update the data with invalid data + channel = self.make_request( + "PUT", + session_endpoint, + {"sequence_token": sequence_token, "data": too_long_data}, + access_token=None, + ) + self.assertEqual(channel.code, 413) + self.assertEqual(channel.json_body["errcode"], "M_TOO_LARGE") + + @parameterized.expand( + [ + ("Sec-Fetch-Dest", "document"), + ("Sec-Fetch-Dest", "image"), + ("Sec-Fetch-Dest", "iframe"), + ("Sec-Fetch-Dest", "embed"), + ("Sec-Fetch-Dest", "video"), + ("Sec-Fetch-Mode", "navigate"), + ("Sec-Fetch-User", "?1"), + ("Sec-Fetch-Site", "none"), + ] + ) + @override_config( + { + "disable_registration": True, + "matrix_authentication_service": { + "enabled": True, + "secret": "secret_value", + "endpoint": "https://issuer", + }, + "experimental_features": { + "msc4388_mode": "public", + }, + } + ) + def test_rendezvous_rejects_unsafe_get_requests( + self, header_name: str, header_value: str + ) -> None: + """ + Tests that GET requests have the appropriate Sec-Fetch-* controls applied as per the MSC. + The mode is set to `public` but this doesn't actually matter. + """ + # We can post arbitrary data to the endpoint + channel = self.make_request( + "POST", + rz_endpoint, + {"data": "foo=bar"}, + access_token=None, + ) + self.assertEqual(channel.code, 200) + rendezvous_id = channel.json_body["id"] + + session_endpoint = rz_endpoint + f"/{rendezvous_id}" + + # We can get the data back + channel = self.make_request( + "GET", + session_endpoint, + access_token=None, + ) + + self.assertEqual(channel.code, 200) + + channel = self.make_request( + "GET", + session_endpoint, + access_token=None, + custom_headers=[(header_name, header_value)], + ) + self.assertEqual(channel.code, 403) + self.assertEqual(channel.json_body["errcode"], "M_FORBIDDEN") + + @override_config( + { + "disable_registration": True, + "matrix_authentication_service": { + "enabled": True, + "secret": "secret_value", + "endpoint": "https://issuer", + }, + "experimental_features": { + "msc4388_mode": "public", + }, + } + ) + def test_rendezvous_allows_from_browser_fetch(self) -> None: + """ + We check that the GET policy does allow for an expected browser fetch + The mode is set to `public` but this doesn't actually matter. + """ + # We can post arbitrary data to the endpoint + channel = self.make_request( + "POST", + rz_endpoint, + {"data": "foo=bar"}, + access_token=None, + ) + self.assertEqual(channel.code, 200) + rendezvous_id = channel.json_body["id"] + + session_endpoint = rz_endpoint + f"/{rendezvous_id}" + + # We can get the data back + channel = self.make_request( + "GET", + session_endpoint, + access_token=None, + ) + + self.assertEqual(channel.code, 200) + + # Test for a typical browser fetch from a client hosted on a different origin + channel = self.make_request( + "GET", + session_endpoint, + access_token=None, + custom_headers=[ + ("Sec-Fetch-Dest", "empty"), + ("Sec-Fetch-Mode", "cors"), + ("Sec-Fetch-Site", "cross-site"), + ], + ) + + self.assertEqual(channel.code, 200) From 2c73e8daef1c50abe9741f1fa321a34f9fdc5909 Mon Sep 17 00:00:00 2001 From: Erik Johnston Date: Thu, 26 Feb 2026 22:41:06 +0100 Subject: [PATCH 039/121] Allow long lived syncs to be cancelled if client has gone away (#19499) --- changelog.d/19499.misc | 1 + pyproject.toml | 6 + synapse/api/auth/mas.py | 3 +- synapse/api/auth/msc3861_delegated.py | 3 +- synapse/appservice/api.py | 3 +- synapse/config/cache.py | 9 +- synapse/federation/federation_server.py | 6 +- synapse/handlers/room.py | 2 +- synapse/handlers/room_list.py | 3 +- synapse/handlers/sync.py | 7 +- synapse/handlers/typing.py | 4 +- synapse/replication/http/_base.py | 2 +- synapse/rest/client/sync.py | 2 + synapse/util/async_helpers.py | 70 ++++- synapse/util/caches/response_cache.py | 152 +++++++++- synapse/util/clock.py | 16 +- synapse/util/wheel_timer.py | 12 +- tests/rest/client/test_sync.py | 63 +++++ tests/util/caches/test_response_cache.py | 345 ++++++++++++++++++++++- tests/util/test_async_helpers.py | 22 +- tests/util/test_wheel_timer.py | 11 +- 21 files changed, 703 insertions(+), 39 deletions(-) create mode 100644 changelog.d/19499.misc diff --git a/changelog.d/19499.misc b/changelog.d/19499.misc new file mode 100644 index 00000000000..c2641c1e58d --- /dev/null +++ b/changelog.d/19499.misc @@ -0,0 +1 @@ +Cancel long-running sync requests if the client has gone away. diff --git a/pyproject.toml b/pyproject.toml index 0b8dff90589..2d55d3c7a59 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -411,6 +411,12 @@ indent-style = "space" skip-magic-trailing-comma = false line-ending = "auto" +[tool.ruff.lint.flake8-bugbear] +extend-immutable-calls = [ + # Durations are immutable + "synapse.util.duration.Duration", +] + [tool.maturin] manifest-path = "rust/Cargo.toml" module-name = "synapse.synapse_rust" diff --git a/synapse/api/auth/mas.py b/synapse/api/auth/mas.py index c4cca3723c6..79c15a53296 100644 --- a/synapse/api/auth/mas.py +++ b/synapse/api/auth/mas.py @@ -45,6 +45,7 @@ from synapse.types import JsonDict, Requester, UserID, create_requester from synapse.util.caches.cached_call import RetryOnExceptionCachedCall from synapse.util.caches.response_cache import ResponseCache, ResponseCacheContext +from synapse.util.duration import Duration from synapse.util.json import json_decoder from . import introspection_response_timer @@ -139,7 +140,7 @@ def __init__(self, hs: "HomeServer"): clock=self._clock, name="mas_token_introspection", server_name=self.server_name, - timeout_ms=120_000, + timeout=Duration(minutes=2), # don't log because the keys are access tokens enable_logging=False, ) diff --git a/synapse/api/auth/msc3861_delegated.py b/synapse/api/auth/msc3861_delegated.py index 7999d6e4598..27ab4af8056 100644 --- a/synapse/api/auth/msc3861_delegated.py +++ b/synapse/api/auth/msc3861_delegated.py @@ -49,6 +49,7 @@ from synapse.types import Requester, UserID, create_requester from synapse.util.caches.cached_call import RetryOnExceptionCachedCall from synapse.util.caches.response_cache import ResponseCache, ResponseCacheContext +from synapse.util.duration import Duration from synapse.util.json import json_decoder from . import introspection_response_timer @@ -205,7 +206,7 @@ def __init__(self, hs: "HomeServer"): clock=self._clock, name="token_introspection", server_name=self.server_name, - timeout_ms=120_000, + timeout=Duration(minutes=2), # don't log because the keys are access tokens enable_logging=False, ) diff --git a/synapse/appservice/api.py b/synapse/appservice/api.py index 71094de9bed..2bbf77a352c 100644 --- a/synapse/appservice/api.py +++ b/synapse/appservice/api.py @@ -46,6 +46,7 @@ from synapse.metrics import SERVER_NAME_LABEL from synapse.types import DeviceListUpdates, JsonDict, JsonMapping, ThirdPartyInstanceID from synapse.util.caches.response_cache import ResponseCache +from synapse.util.duration import Duration if TYPE_CHECKING: from synapse.server import HomeServer @@ -132,7 +133,7 @@ def __init__(self, hs: "HomeServer"): clock=hs.get_clock(), name="as_protocol_meta", server_name=self.server_name, - timeout_ms=HOUR_IN_MS, + timeout=Duration(hours=1), ) def _get_headers(self, service: "ApplicationService") -> dict[bytes, list[bytes]]: diff --git a/synapse/config/cache.py b/synapse/config/cache.py index c9ce826e1a2..ac94b17ff6e 100644 --- a/synapse/config/cache.py +++ b/synapse/config/cache.py @@ -29,6 +29,7 @@ from synapse.types import JsonDict from synapse.util.check_dependencies import check_requirements +from synapse.util.duration import Duration from ._base import Config, ConfigError @@ -108,7 +109,7 @@ class CacheConfig(Config): global_factor: float track_memory_usage: bool expiry_time_msec: int | None - sync_response_cache_duration: int + sync_response_cache_duration: Duration @staticmethod def reset() -> None: @@ -207,10 +208,14 @@ def read_config(self, config: JsonDict, **kwargs: Any) -> None: min_cache_ttl = self.cache_autotuning.get("min_cache_ttl") self.cache_autotuning["min_cache_ttl"] = self.parse_duration(min_cache_ttl) - self.sync_response_cache_duration = self.parse_duration( + sync_response_cache_duration_ms = self.parse_duration( cache_config.get("sync_response_cache_duration", "2m") ) + self.sync_response_cache_duration = Duration( + milliseconds=sync_response_cache_duration_ms + ) + def resize_all_caches(self) -> None: """Ensure all cache sizes are up-to-date. diff --git a/synapse/federation/federation_server.py b/synapse/federation/federation_server.py index b909f1e5956..1912d545f55 100644 --- a/synapse/federation/federation_server.py +++ b/synapse/federation/federation_server.py @@ -166,7 +166,7 @@ def __init__(self, hs: "HomeServer"): clock=hs.get_clock(), name="fed_txn_handler", server_name=self.server_name, - timeout_ms=30000, + timeout=Duration(seconds=30), ) self.transaction_actions = TransactionActions(self.store) @@ -179,13 +179,13 @@ def __init__(self, hs: "HomeServer"): clock=hs.get_clock(), name="state_resp", server_name=self.server_name, - timeout_ms=30000, + timeout=Duration(seconds=30), ) self._state_ids_resp_cache: ResponseCache[tuple[str, str]] = ResponseCache( clock=hs.get_clock(), name="state_ids_resp", server_name=self.server_name, - timeout_ms=30000, + timeout=Duration(seconds=30), ) self._federation_metrics_domains = ( diff --git a/synapse/handlers/room.py b/synapse/handlers/room.py index e03a9123198..1c3489a00e3 100644 --- a/synapse/handlers/room.py +++ b/synapse/handlers/room.py @@ -185,7 +185,7 @@ def __init__(self, hs: "HomeServer"): clock=hs.get_clock(), name="room_upgrade", server_name=self.server_name, - timeout_ms=FIVE_MINUTES_IN_MS, + timeout=Duration(minutes=5), ) self._server_notices_mxid = hs.config.servernotices.server_notices_mxid diff --git a/synapse/handlers/room_list.py b/synapse/handlers/room_list.py index 6377931b392..b25fd0a1e71 100644 --- a/synapse/handlers/room_list.py +++ b/synapse/handlers/room_list.py @@ -44,6 +44,7 @@ from synapse.types import JsonDict, JsonMapping, ThirdPartyInstanceID from synapse.util.caches.descriptors import _CacheContext, cached from synapse.util.caches.response_cache import ResponseCache +from synapse.util.duration import Duration if TYPE_CHECKING: from synapse.server import HomeServer @@ -79,7 +80,7 @@ def __init__(self, hs: "HomeServer"): clock=hs.get_clock(), name="remote_room_list", server_name=self.server_name, - timeout_ms=30 * 1000, + timeout=Duration(seconds=30), ) async def get_local_public_room_list( diff --git a/synapse/handlers/sync.py b/synapse/handlers/sync.py index 72e91d66ac4..2f405004de9 100644 --- a/synapse/handlers/sync.py +++ b/synapse/handlers/sync.py @@ -77,6 +77,7 @@ from synapse.util.caches.expiringcache import ExpiringCache from synapse.util.caches.lrucache import LruCache from synapse.util.caches.response_cache import ResponseCache, ResponseCacheContext +from synapse.util.cancellation import cancellable from synapse.util.metrics import Measure from synapse.visibility import filter_and_transform_events_for_client @@ -307,7 +308,7 @@ def __init__(self, hs: "HomeServer"): clock=hs.get_clock(), name="sync", server_name=self.server_name, - timeout_ms=hs.config.caches.sync_response_cache_duration, + timeout=hs.config.caches.sync_response_cache_duration, ) # ExpiringCache((User, Device)) -> LruCache(user_id => event_id) @@ -367,6 +368,10 @@ async def wait_for_sync_for_user( logger.debug("Returning sync response for %s", user_id) return res + # TODO: We mark this as cancellable, and we have tests for it, but we + # haven't gone through and exhaustively checked that all the code paths in + # this method are actually cancellable. + @cancellable async def _wait_for_sync_for_user( self, sync_config: SyncConfig, diff --git a/synapse/handlers/typing.py b/synapse/handlers/typing.py index e66396fecc2..6daf304432e 100644 --- a/synapse/handlers/typing.py +++ b/synapse/handlers/typing.py @@ -102,7 +102,7 @@ def __init__(self, hs: "HomeServer"): self._room_typing: dict[str, set[str]] = {} self._member_last_federation_poke: dict[RoomMember, int] = {} - self.wheel_timer: WheelTimer[RoomMember] = WheelTimer(bucket_size=5000) + self.wheel_timer: WheelTimer[RoomMember] = WheelTimer() self._latest_room_serial = 0 self._rooms_updated: set[str] = set() @@ -120,7 +120,7 @@ def _reset(self) -> None: self._rooms_updated = set() self._member_last_federation_poke = {} - self.wheel_timer = WheelTimer(bucket_size=5000) + self.wheel_timer = WheelTimer() @wrap_as_background_process("typing._handle_timeouts") async def _handle_timeouts(self) -> None: diff --git a/synapse/replication/http/_base.py b/synapse/replication/http/_base.py index 2bab9c2d713..87d6e808982 100644 --- a/synapse/replication/http/_base.py +++ b/synapse/replication/http/_base.py @@ -130,7 +130,7 @@ def __init__(self, hs: "HomeServer"): clock=hs.get_clock(), name="repl." + self.NAME, server_name=self.server_name, - timeout_ms=30 * 60 * 1000, + timeout=Duration(minutes=30), ) # We reserve `instance_name` as a parameter to sending requests, so we diff --git a/synapse/rest/client/sync.py b/synapse/rest/client/sync.py index 458bf08a19f..91f2f16771a 100644 --- a/synapse/rest/client/sync.py +++ b/synapse/rest/client/sync.py @@ -59,6 +59,7 @@ from synapse.types import JsonDict, Requester, SlidingSyncStreamToken, StreamToken from synapse.types.rest.client import SlidingSyncBody from synapse.util.caches.lrucache import LruCache +from synapse.util.cancellation import cancellable from synapse.util.json import json_decoder from ._base import client_patterns, set_timeline_upper_limit @@ -138,6 +139,7 @@ def __init__(self, hs: "HomeServer"): cfg=hs.config.ratelimiting.rc_presence_per_user, ) + @cancellable async def on_GET(self, request: SynapseRequest) -> tuple[int, JsonDict]: # This will always be set by the time Twisted calls us. assert request.args is not None diff --git a/synapse/util/async_helpers.py b/synapse/util/async_helpers.py index 818f8b1a69b..53fb24ec5b8 100644 --- a/synapse/util/async_helpers.py +++ b/synapse/util/async_helpers.py @@ -57,6 +57,7 @@ run_coroutine_in_background, run_in_background, ) +from synapse.util.cancellation import cancellable from synapse.util.clock import Clock from synapse.util.duration import Duration @@ -83,6 +84,13 @@ def observe(self) -> "defer.Deferred[_T]": """ ... + @abc.abstractmethod + def has_observers(self) -> bool: + """Returns True if there are any observers currently observing this + ObservableDeferred. + """ + ... + class ObservableDeferred(Generic[_T], AbstractObservableDeferred[_T]): """Wraps a deferred object so that we can add observer deferreds. These @@ -122,6 +130,11 @@ def callback(r: _T) -> _T: for observer in observers: try: observer.callback(r) + except defer.CancelledError: + # We do not want to propagate cancellations to the original + # deferred, or to other observers, so we can just ignore + # this. + pass except Exception as e: logger.exception( "%r threw an exception on .callback(%r), ignoring...", @@ -145,6 +158,11 @@ def errback(f: Failure) -> Failure | None: f.value.__failure__ = f try: observer.errback(f) + except defer.CancelledError: + # We do not want to propagate cancellations to the original + # deferred, or to other observers, so we can just ignore + # this. + pass except Exception as e: logger.exception( "%r threw an exception on .errback(%r), ignoring...", @@ -160,6 +178,7 @@ def errback(f: Failure) -> Failure | None: deferred.addCallbacks(callback, errback) + @cancellable def observe(self) -> "defer.Deferred[_T]": """Observe the underlying deferred. @@ -169,7 +188,7 @@ def observe(self) -> "defer.Deferred[_T]": """ if not self._result: assert isinstance(self._observers, list) - d: "defer.Deferred[_T]" = defer.Deferred() + d: "defer.Deferred[_T]" = defer.Deferred(canceller=self._remove_observer) self._observers.append(d) return d elif self._result[0]: @@ -180,6 +199,12 @@ def observe(self) -> "defer.Deferred[_T]": def observers(self) -> "Collection[defer.Deferred[_T]]": return self._observers + def has_observers(self) -> bool: + """Returns True if there are any observers currently observing this + ObservableDeferred. + """ + return bool(self._observers) + def has_called(self) -> bool: return self._result is not None @@ -204,6 +229,28 @@ def __repr__(self) -> str: self._deferred, ) + def _remove_observer(self, observer: "defer.Deferred[_T]") -> None: + """Removes an observer from the list of observers. + + Used as a canceller for the observer deferreds, so that if an observer + is cancelled it is removed from the list of observers. + """ + if self._result is not None: + # The underlying deferred has already resolved, so the observer has + # already been resolved. Nothing to do. + return + + assert isinstance(self._observers, list) + try: + self._observers.remove(observer) + except ValueError: + # The observer was not in the list. This can happen if the underlying + # deferred resolves at around the same time as we try to remove the + # observer. In this case, it's possible that we tried to remove the + # observer just after it was added to the list, but before it was + # resolved and removed from the list by the callback/errback above. + pass + T = TypeVar("T") @@ -962,6 +1009,27 @@ def handle_cancel(new_deferred: "defer.Deferred[T]") -> None: return new_deferred +def observe_deferred(d: "defer.Deferred[T]") -> "defer.Deferred[T]": + """Returns a new `Deferred` that observes the given `Deferred`. + + The returned `Deferred` will resolve with the same result as the given + `Deferred`, but will not "chain" on the deferred so that using the returned + deferred does not affect the given `Deferred` in any way. + """ + new_deferred: "defer.Deferred[T]" = defer.Deferred() + + def callback(r: T) -> T: + new_deferred.callback(r) + return r + + def errback(f: Failure) -> Failure: + new_deferred.errback(f) + return f + + d.addCallbacks(callback, errback) + return new_deferred + + class AwakenableSleeper: """Allows explicitly waking up deferreds related to an entity that are currently sleeping. diff --git a/synapse/util/caches/response_cache.py b/synapse/util/caches/response_cache.py index 0289e13f6a0..70cea9b77c0 100644 --- a/synapse/util/caches/response_cache.py +++ b/synapse/util/caches/response_cache.py @@ -39,10 +39,15 @@ start_active_span, start_active_span_follows_from, ) -from synapse.util.async_helpers import AbstractObservableDeferred, ObservableDeferred +from synapse.util.async_helpers import ( + ObservableDeferred, + delay_cancellation, +) from synapse.util.caches import EvictionReason, register_cache +from synapse.util.cancellation import cancellable, is_function_cancellable from synapse.util.clock import Clock from synapse.util.duration import Duration +from synapse.util.wheel_timer import WheelTimer logger = logging.getLogger(__name__) @@ -79,8 +84,8 @@ class ResponseCacheContext(Generic[KV]): @attr.s(auto_attribs=True) -class ResponseCacheEntry: - result: AbstractObservableDeferred +class ResponseCacheEntry(Generic[KV]): + result: ObservableDeferred[KV] """The (possibly incomplete) result of the operation. Note that we continue to store an ObservableDeferred even after the operation @@ -91,6 +96,15 @@ class ResponseCacheEntry: opentracing_span_context: "opentracing.SpanContext | None" """The opentracing span which generated/is generating the result""" + cancellable: bool + """Whether the deferred is safe to be cancelled.""" + + last_observer_removed_time_ms: int | None = None + """The last time that an observer was removed from this entry. + + Used to determine when to evict the entry if it has no observers. + """ + class ResponseCache(Generic[KV]): """ @@ -98,6 +112,22 @@ class ResponseCache(Generic[KV]): returned from the cache. This means that if the client retries the request while the response is still being computed, that original response will be used rather than trying to compute a new response. + + If a timeout is not specified then the cache entry will be kept while the + wrapped function is still running, and will be removed immediately once it + completes. + + If a timeout is specified then the cache entry will be kept for the duration + of the timeout after the wrapped function completes. If the wrapped function + is cancellable and during processing nothing waits on the result for longer + than the timeout then the wrapped function will be cancelled and the cache + entry will be removed. + + This behaviour is useful for caching responses to requests which are + expensive to compute, but which may be retried by clients if they time out. + For example, /sync requests which may take a long time to compute, and which + clients will retry. However, if the client stops retrying for a while then + we want to stop processing the request and free up the resources. """ def __init__( @@ -106,7 +136,7 @@ def __init__( clock: Clock, name: str, server_name: str, - timeout_ms: float = 0, + timeout: Duration | None = None, enable_logging: bool = True, ): """ @@ -121,7 +151,7 @@ def __init__( self._result_cache: dict[KV, ResponseCacheEntry] = {} self.clock = clock - self.timeout = Duration(milliseconds=timeout_ms) + self.timeout = timeout self._name = name self._metrics = register_cache( @@ -133,6 +163,13 @@ def __init__( ) self._enable_logging = enable_logging + self._prune_timer: WheelTimer[KV] | None = None + if self.timeout: + # Set up the timers for pruning inflight entries. The times here are + # how often we check for entries to prune. + self._prune_timer = WheelTimer(bucket_size=self.timeout / 10) + self.clock.looping_call(self._prune_inflight_entries, self.timeout / 10) + def size(self) -> int: return len(self._result_cache) @@ -172,6 +209,7 @@ def _set( context: ResponseCacheContext[KV], deferred: "defer.Deferred[RV]", opentracing_span_context: "opentracing.SpanContext | None", + cancellable: bool, ) -> ResponseCacheEntry: """Set the entry for the given key to the given deferred. @@ -183,13 +221,16 @@ def _set( context: Information about the cache miss deferred: The deferred which resolves to the result. opentracing_span_context: An opentracing span wrapping the calculation + cancellable: Whether the deferred is safe to be cancelled Returns: The cache entry object. """ result = ObservableDeferred(deferred, consumeErrors=True) key = context.cache_key - entry = ResponseCacheEntry(result, opentracing_span_context) + entry = ResponseCacheEntry( + result, opentracing_span_context, cancellable=cancellable + ) self._result_cache[key] = entry def on_complete(r: RV) -> RV: @@ -233,6 +274,7 @@ def _entry_timeout(self, key: KV) -> None: self._metrics.inc_evictions(EvictionReason.time) self._result_cache.pop(key, None) + @cancellable async def wrap( self, key: KV, @@ -301,8 +343,44 @@ async def cb() -> RV: return await callback(*args, **kwargs) d = run_in_background(cb) - entry = self._set(context, d, span_context) - return await make_deferred_yieldable(entry.result.observe()) + entry = self._set( + context, d, span_context, cancellable=is_function_cancellable(callback) + ) + try: + return await make_deferred_yieldable(entry.result.observe()) + except defer.CancelledError: + pass + + # We've been cancelled. + # + # Since we've kicked off the background operation, we can't just + # give up and return here and need to wait for the background + # operation to stop. We don't want to stop the background process + # immediately to give a chance for retries to come in and wait for + # the result. + # + # Instead, we temporarily swallow the cancellation and mark the + # cache key as one to potentially timeout. + + # Update the `last_observer_removed_time_ms` so that the pruning + # mechanism can kick in if needed. + now = self.clock.time_msec() + entry.last_observer_removed_time_ms = now + if self._prune_timer is not None and self.timeout: + self._prune_timer.insert(now, key, now + self.timeout.as_millis()) + + # Wait on the original deferred, which will continue to run in the + # background until it completes. We don't want to add an observer as + # this would prevent the entry from being pruned. + # + # Note that this deferred has been consumed by the + # ObservableDeferred, so we don't know what it will return. That + # doesn't matter as we just want to throw a CancelledError once it completes anyway. + try: + await make_deferred_yieldable(delay_cancellation(d)) + except Exception: + pass + raise defer.CancelledError() result = entry.result.observe() if self._enable_logging: @@ -320,4 +398,60 @@ async def cb() -> RV: f"ResponseCache[{self._name}].wait", contexts=(span_context,) if span_context else (), ): - return await make_deferred_yieldable(result) + try: + return await make_deferred_yieldable(result) + except defer.CancelledError: + # If we're cancelled then we update the + # `last_observer_removed_time_ms` so that the pruning mechanism + # can kick in if needed. + now = self.clock.time_msec() + entry.last_observer_removed_time_ms = now + if self._prune_timer is not None and self.timeout: + self._prune_timer.insert(now, key, now + self.timeout.as_millis()) + raise + + def _prune_inflight_entries(self) -> None: + """Prune entries which have been in the cache for too long without + observers""" + assert self._prune_timer is not None + assert self.timeout is not None + + now = self.clock.time_msec() + keys_to_check = self._prune_timer.fetch(now) + + # Loop through the keys and check if they should be evicted. We evict + # entries which have no active observers, and which have been in the + # cache for longer than the timeout since the last observer was removed. + for key in keys_to_check: + entry = self._result_cache.get(key) + if not entry: + continue + + if not entry.cancellable: + # this entry is not cancellable, so we should keep it in the cache until it completes. + continue + + if entry.result.has_called(): + # this entry has already completed, so we should have scheduled it for + # removal at the right time. We can just skip it here and wait for the + # scheduled call to remove it. + continue + + if entry.result.has_observers(): + # this entry has observers, so we should keep it in the cache for now. + continue + + if entry.last_observer_removed_time_ms is None: + # this should never happen, but just in case, we should keep the entry + # in the cache until we have a valid last_observer_removed_time_ms to + # compare against. + continue + + if now - entry.last_observer_removed_time_ms > self.timeout.as_millis(): + self._metrics.inc_evictions(EvictionReason.time) + self._result_cache.pop(key, None) + try: + entry.result.cancel() + except Exception: + # we ignore exceptions from cancel, as it is best effort anyway. + pass diff --git a/synapse/util/clock.py b/synapse/util/clock.py index a3872d6f939..7232a1331c8 100644 --- a/synapse/util/clock.py +++ b/synapse/util/clock.py @@ -62,6 +62,16 @@ logging.setLoggerClass(original_logger_class) +def _try_wakeup_deferred(d: Deferred) -> None: + """Try to wake up a deferred, but ignore any exceptions raised by the + callback. This is useful when we want to wake up a deferred that may have + already been cancelled, and we don't care about the result.""" + try: + d.callback(None) + except Exception: + pass + + class Clock: """ A Clock wraps a Twisted reactor and provides utilities on top of it. @@ -114,7 +124,11 @@ async def sleep(self, duration: Duration) -> None: with context.PreserveLoggingContext(): # We can ignore the lint here since this class is the one location callLater should # be called. - self._reactor.callLater(duration.as_secs(), d.callback, duration.as_secs()) # type: ignore[call-later-not-tracked] + self._reactor.callLater( + duration.as_secs(), + lambda _: _try_wakeup_deferred(d), + duration.as_secs(), + ) # type: ignore[call-later-not-tracked] await d def time(self) -> float: diff --git a/synapse/util/wheel_timer.py b/synapse/util/wheel_timer.py index c63faa96dfb..fe4622fe99b 100644 --- a/synapse/util/wheel_timer.py +++ b/synapse/util/wheel_timer.py @@ -23,6 +23,8 @@ import attr +from synapse.util.duration import Duration + logger = logging.getLogger(__name__) T = TypeVar("T", bound=Hashable) @@ -39,13 +41,13 @@ class WheelTimer(Generic[T]): expired. """ - def __init__(self, bucket_size: int = 5000) -> None: + def __init__(self, bucket_size: Duration = Duration(seconds=5)) -> None: """ Args: bucket_size: Size of buckets in ms. Corresponds roughly to the accuracy of the timer. """ - self.bucket_size: int = bucket_size + self.bucket_size = bucket_size self.entries: list[_Entry[T]] = [] def insert(self, now: int, obj: T, then: int) -> None: @@ -56,8 +58,8 @@ def insert(self, now: int, obj: T, then: int) -> None: obj: Object to be inserted then: When to return the object strictly after. """ - then_key = int(then / self.bucket_size) + 1 - now_key = int(now / self.bucket_size) + then_key = int(then / self.bucket_size.as_millis()) + 1 + now_key = int(now / self.bucket_size.as_millis()) if self.entries: min_key = self.entries[0].end_key @@ -100,7 +102,7 @@ def fetch(self, now: int) -> list[T]: Returns: List of objects that have timed out """ - now_key = int(now / self.bucket_size) + now_key = int(now / self.bucket_size.as_millis()) ret: list[T] = [] while self.entries and self.entries[0].end_key <= now_key: diff --git a/tests/rest/client/test_sync.py b/tests/rest/client/test_sync.py index fcbf3fd53cd..e6ada1adb27 100644 --- a/tests/rest/client/test_sync.py +++ b/tests/rest/client/test_sync.py @@ -41,6 +41,7 @@ from tests.federation.transport.test_knocking import ( KnockingStrippedStateEventHelperMixin, ) +from tests.rest.client.test_rooms import make_request_with_cancellation_test from tests.server import TimedOutException logger = logging.getLogger(__name__) @@ -1145,3 +1146,65 @@ def test_incremental_sync(self) -> None: self.assertNotIn(self.excluded_room_id, channel.json_body["rooms"]["join"]) self.assertIn(self.included_room_id, channel.json_body["rooms"]["join"]) + + +class SyncCancellationTestCase(unittest.HomeserverTestCase): + servlets = [ + synapse.rest.admin.register_servlets, + login.register_servlets, + sync.register_servlets, + room.register_servlets, + ] + + def test_initial_sync(self) -> None: + """Tests that an initial sync request can be cancelled.""" + user_id = self.register_user("user", "password") + tok = self.login("user", "password") + + # Populate the account with a few rooms + for _ in range(5): + room_id = self.helper.create_room_as(user_id, tok=tok) + self.helper.send(room_id, tok=tok) + + channel = make_request_with_cancellation_test( + "test_initial_sync", + self.reactor, + self.site, + "GET", + "/_matrix/client/v3/sync", + token=tok, + ) + + self.assertEqual(200, channel.code, msg=channel.result["body"]) + + def test_incremental_sync(self) -> None: + """Tests that an incremental sync request can be cancelled.""" + user_id = self.register_user("user", "password") + tok = self.login("user", "password") + + # Populate the account with a few rooms + room_ids = [] + for _ in range(5): + room_id = self.helper.create_room_as(user_id, tok=tok) + self.helper.send(room_id, tok=tok) + room_ids.append(room_id) + + # Do an initial sync to get a since token. + channel = self.make_request("GET", "/sync", access_token=tok) + self.assertEqual(200, channel.code, msg=channel.result) + since = channel.json_body["next_batch"] + + # Send some more messages to generate activity in the rooms. + for room_id in room_ids: + self.helper.send(room_id, tok=tok) + + channel = make_request_with_cancellation_test( + "test_incremental_sync", + self.reactor, + self.site, + "GET", + f"/_matrix/client/v3/sync?since={since}&timeout=10000", + token=tok, + ) + + self.assertEqual(200, channel.code, msg=channel.result["body"]) diff --git a/tests/util/caches/test_response_cache.py b/tests/util/caches/test_response_cache.py index def5c817db2..af427698901 100644 --- a/tests/util/caches/test_response_cache.py +++ b/tests/util/caches/test_response_cache.py @@ -19,6 +19,7 @@ # # +from functools import wraps from unittest.mock import Mock from parameterized import parameterized @@ -26,6 +27,7 @@ from twisted.internet import defer from synapse.util.caches.response_cache import ResponseCache, ResponseCacheContext +from synapse.util.cancellation import cancellable from synapse.util.duration import Duration from tests.server import get_clock @@ -48,15 +50,23 @@ def setUp(self) -> None: def with_cache(self, name: str, ms: int = 0) -> ResponseCache: return ResponseCache( - clock=self.clock, name=name, server_name="test_server", timeout_ms=ms + clock=self.clock, + name=name, + server_name="test_server", + timeout=Duration(milliseconds=ms), ) @staticmethod async def instant_return(o: str) -> str: return o - async def delayed_return(self, o: str) -> str: - await self.clock.sleep(Duration(seconds=1)) + @cancellable + async def delayed_return( + self, + o: str, + duration: Duration = Duration(seconds=1), # noqa + ) -> str: + await self.clock.sleep(duration) return o def test_cache_hit(self) -> None: @@ -223,3 +233,332 @@ async def non_caching(o: str, cache_context: ResponseCacheContext[int]) -> str: self.assertCountEqual( [], cache.keys(), "cache should not have the result now" ) + + def test_cache_func_errors(self) -> None: + """If the callback raises an error, the error should be raised to all + callers and the result should not be cached""" + cache = self.with_cache("error_cache", ms=3000) + + expected_error = Exception("oh no") + + async def erring(o: str) -> str: + await self.clock.sleep(Duration(seconds=1)) + raise expected_error + + wrap_d = defer.ensureDeferred(cache.wrap(0, erring, "ignored")) + self.assertNoResult(wrap_d) + + # a second call should also return a pending deferred + wrap2_d = defer.ensureDeferred(cache.wrap(0, erring, "ignored")) + self.assertNoResult(wrap2_d) + + # let the call complete + self.reactor.advance(1) + + # both results should have completed with the error + self.assertFailure(wrap_d, Exception) + self.assertFailure(wrap2_d, Exception) + + def test_cache_cancel_first_wait(self) -> None: + """Test that cancellation of the deferred returned by wrap() on the + first call does not immediately cause a cancellation error to be raised + when its cancelled and the wrapped function continues execution (unless + it times out). + """ + cache = self.with_cache("cancel_cache", ms=3000) + + expected_result = "howdy" + + wrap_d = defer.ensureDeferred( + cache.wrap(0, self.delayed_return, expected_result) + ) + + # cancel the deferred before it has a chance to return + wrap_d.cancel() + + # The cancel should be ignored for now, and the inner function should + # still be running. + self.assertNoResult(wrap_d) + + # Advance the clock until the inner function should have returned, but + # not long enough for the cache entry to have expired. + self.reactor.advance(2) + + # The deferred we're waiting on should now return a cancelled error. + self.assertFailure(wrap_d, defer.CancelledError) + + # However future callers should get the result. + wrap_d2 = defer.ensureDeferred( + cache.wrap(0, self.delayed_return, expected_result) + ) + self.assertEqual(expected_result, self.successResultOf(wrap_d2)) + + def test_cache_cancel_first_wait_expire(self) -> None: + """Test that cancellation of the deferred returned by wrap() and the + entry expiring before the wrapped function returns. + + The wrapped function should be cancelled. + """ + cache = self.with_cache("cancel_expire_cache", ms=300) + + expected_result = "howdy" + + # Wrap the function so that we can keep track of when it completes or + # errors. + completed = False + cancelled = False + + @wraps(self.delayed_return) + async def wrapped(o: str) -> str: + nonlocal completed, cancelled + + try: + return await self.delayed_return(o) + except defer.CancelledError: + cancelled = True + raise + finally: + completed = True + + wrap_d = defer.ensureDeferred(cache.wrap(0, wrapped, expected_result)) + + # cancel the deferred before it has a chance to return + wrap_d.cancel() + + # The cancel should be ignored for now, and the inner function should + # still be running. + self.assertNoResult(wrap_d) + self.assertFalse(completed, "wrapped function should not have completed yet") + + # Advance the clock until the cache entry should have expired, but not + # long enough for the inner function to have returned. + self.reactor.advance(0.7) + + # The deferred we're waiting on should now return a cancelled error. + self.assertFailure(wrap_d, defer.CancelledError) + self.assertTrue(completed, "wrapped function should have completed") + self.assertTrue(cancelled, "wrapped function should have been cancelled") + + def test_cache_cancel_first_wait_other_observers(self) -> None: + """Test that cancellation of the deferred returned by wrap() does not + cause a cancellation error to be raised if there are other observers + still waiting on the result. + """ + cache = self.with_cache("cancel_other_cache", ms=300) + + expected_result = "howdy" + + # Wrap the function so that we can keep track of when it completes or + # errors. + completed = False + cancelled = False + + @wraps(self.delayed_return) + async def wrapped(o: str) -> str: + nonlocal completed, cancelled + + try: + return await self.delayed_return(o) + except defer.CancelledError: + cancelled = True + raise + finally: + completed = True + + wrap_d1 = defer.ensureDeferred(cache.wrap(0, wrapped, expected_result)) + wrap_d2 = defer.ensureDeferred(cache.wrap(0, wrapped, expected_result)) + + # cancel the first deferred before it has a chance to return + wrap_d1.cancel() + + # The cancel should be ignored for now, and the inner function should + # still be running. + self.assertNoResult(wrap_d1) + self.assertNoResult(wrap_d2) + self.assertFalse(completed, "wrapped function should not have completed yet") + + # Advance the clock until the cache entry should have expired, but not + # long enough for the inner function to have returned. + self.reactor.advance(0.7) + + # Neither deferred should have returned yet, since the inner function + # should still be running. + self.assertNoResult(wrap_d1) + self.assertNoResult(wrap_d2) + self.assertFalse(completed, "wrapped function should not have completed yet") + + # Now advance the clock until the inner function should have returned. + self.reactor.advance(2.5) + + # The wrapped function should have completed without cancellation. + self.assertTrue(completed, "wrapped function should have completed") + self.assertFalse(cancelled, "wrapped function should not have been cancelled") + + # The first deferred we're waiting on should now return a cancelled error. + self.assertFailure(wrap_d1, defer.CancelledError) + + # The second deferred should return the result. + self.assertEqual(expected_result, self.successResultOf(wrap_d2)) + + def test_cache_add_and_cancel(self) -> None: + """Test that waiting on the cache and cancelling repeatedly keeps the + cache entry alive. + """ + cache = self.with_cache("cancel_add_cache", ms=300) + + expected_result = "howdy" + + # Wrap the function so that we can keep track of when it completes or + # errors. + completed = False + cancelled = False + + @wraps(self.delayed_return) + async def wrapped(o: str) -> str: + nonlocal completed, cancelled + + try: + return await self.delayed_return(o) + except defer.CancelledError: + cancelled = True + raise + finally: + completed = True + + # Repeatedly await for the result and cancel it, which should keep the + # cache entry alive even though the total time exceeds the cache + # timeout. + deferreds = [] + for _ in range(8): + # Await the deferred. + wrap_d = defer.ensureDeferred(cache.wrap(0, wrapped, expected_result)) + + # cancel the deferred before it has a chance to return + self.reactor.advance(0.05) + wrap_d.cancel() + deferreds.append(wrap_d) + + # The cancel should not cause the inner function to be cancelled + # yet. + self.assertFalse( + completed, "wrapped function should not have completed yet" + ) + self.assertFalse( + cancelled, "wrapped function should not have been cancelled yet" + ) + + # Advance the clock until the cache entry should have expired, but not + # long enough for the inner function to have returned. + self.reactor.advance(0.05) + + # Now advance the clock until the inner function should have returned. + self.reactor.advance(0.2) + + # All the deferreds we're waiting on should now return a cancelled error. + for wrap_d in deferreds: + self.assertFailure(wrap_d, defer.CancelledError) + + # The wrapped function should have completed without cancellation. + self.assertTrue(completed, "wrapped function should have completed") + self.assertFalse(cancelled, "wrapped function should not have been cancelled") + + # Querying the cache should return the completed result + wrap_d = defer.ensureDeferred(cache.wrap(0, wrapped, expected_result)) + self.assertEqual(expected_result, self.successResultOf(wrap_d)) + + def test_cache_cancel_non_cancellable(self) -> None: + """Test that cancellation of the deferred returned by wrap() on a + non-cancellable entry does not cause a cancellation error to be raised + when it's cancelled and the wrapped function continues execution. + """ + cache = self.with_cache("cancel_non_cancellable_cache", ms=300) + + expected_result = "howdy" + + # Wrap the function so that we can keep track of when it completes or + # errors. + completed = False + cancelled = False + + async def wrapped(o: str) -> str: + nonlocal completed, cancelled + + try: + return await self.delayed_return(o) + except defer.CancelledError: + cancelled = True + raise + finally: + completed = True + + wrap_d = defer.ensureDeferred(cache.wrap(0, wrapped, expected_result)) + + # cancel the deferred before it has a chance to return + wrap_d.cancel() + + # The cancel should be ignored for now, and the inner function should + # still be running. + self.assertNoResult(wrap_d) + self.assertFalse(completed, "wrapped function should not have completed yet") + + # Advance the clock until the inner function should have returned, but + # not long enough for the cache entry to have expired. + self.reactor.advance(2) + + # The deferred we're waiting on should be cancelled, but a new call to + # the cache should return the result. + self.assertFailure(wrap_d, defer.CancelledError) + wrap_d2 = defer.ensureDeferred(cache.wrap(0, wrapped, expected_result)) + self.assertEqual(expected_result, self.successResultOf(wrap_d2)) + + def test_cache_cancel_then_error(self) -> None: + """Test that cancellation of the deferred returned by wrap() that then + subsequently errors is correctly propagated to a second caller. + """ + + cache = self.with_cache("cancel_then_error_cache", ms=3000) + + expected_error = Exception("oh no") + + # Wrap the function so that we can keep track of when it completes or + # errors. + completed = False + cancelled = False + + @wraps(self.delayed_return) + async def wrapped(o: str) -> str: + nonlocal completed, cancelled + + try: + await self.delayed_return(o) + raise expected_error + except defer.CancelledError: + cancelled = True + raise + finally: + completed = True + + wrap_d1 = defer.ensureDeferred(cache.wrap(0, wrapped, "ignored")) + wrap_d2 = defer.ensureDeferred(cache.wrap(0, wrapped, "ignored")) + + # cancel the first deferred before it has a chance to return + wrap_d1.cancel() + + # The cancel should be ignored for now, and the inner function should + # still be running. + self.assertNoResult(wrap_d1) + self.assertNoResult(wrap_d2) + self.assertFalse(completed, "wrapped function should not have completed yet") + + # Advance the clock until the inner function should have returned. + self.reactor.advance(2) + + # The wrapped function should have completed with an error without cancellation. + self.assertTrue(completed, "wrapped function should have completed") + self.assertFalse(cancelled, "wrapped function should not have been cancelled") + + # The first deferred we're waiting on should now return a cancelled error. + self.assertFailure(wrap_d1, defer.CancelledError) + + # The second deferred should return the error. + self.assertFailure(wrap_d2, Exception) diff --git a/tests/util/test_async_helpers.py b/tests/util/test_async_helpers.py index a6b7ddf485e..e4b1bb2b235 100644 --- a/tests/util/test_async_helpers.py +++ b/tests/util/test_async_helpers.py @@ -120,7 +120,7 @@ def check_failure(res: Failure, idx: int) -> None: assert results[1] is not None self.assertEqual(str(results[1].value), "gah!", "observer 2 errback result") - def test_cancellation(self) -> None: + def test_cancellation_observer(self) -> None: """Test that cancelling an observer does not affect other observers.""" origin_d: "Deferred[int]" = Deferred() observable = ObservableDeferred(origin_d, consumeErrors=True) @@ -138,6 +138,10 @@ def test_cancellation(self) -> None: self.assertFalse(observer1.called) self.failureResultOf(observer2, CancelledError) self.assertFalse(observer3.called) + # check that we remove the cancelled observer from the list of observers + # as a clean up. + self.assertEqual(len(observable.observers()), 2) + self.assertNotIn(observer2, observable.observers()) # other observers resolve as normal origin_d.callback(123) @@ -148,6 +152,22 @@ def test_cancellation(self) -> None: observer4 = observable.observe() self.assertEqual(observer4.result, 123, "observer 4 callback result") + def test_cancellation_observee(self) -> None: + """Test that cancelling the original deferred cancels all observers.""" + origin_d: "Deferred[int]" = Deferred() + observable = ObservableDeferred(origin_d, consumeErrors=True) + + observer1 = observable.observe() + observer2 = observable.observe() + + self.assertFalse(observer1.called) + self.assertFalse(observer2.called) + + # cancel the original deferred + origin_d.cancel() + self.failureResultOf(observer1, CancelledError) + self.failureResultOf(observer2, CancelledError) + class TimeoutDeferredTest(TestCase): def setUp(self) -> None: diff --git a/tests/util/test_wheel_timer.py b/tests/util/test_wheel_timer.py index 6fa575a18e4..3dd9a9891f3 100644 --- a/tests/util/test_wheel_timer.py +++ b/tests/util/test_wheel_timer.py @@ -19,6 +19,7 @@ # # +from synapse.util.duration import Duration from synapse.util.wheel_timer import WheelTimer from .. import unittest @@ -26,7 +27,7 @@ class WheelTimerTestCase(unittest.TestCase): def test_single_insert_fetch(self) -> None: - wheel: WheelTimer[object] = WheelTimer(bucket_size=5) + wheel: WheelTimer[object] = WheelTimer(bucket_size=Duration(milliseconds=5)) wheel.insert(100, "1", 150) @@ -39,7 +40,7 @@ def test_single_insert_fetch(self) -> None: self.assertListEqual(wheel.fetch(170), []) def test_multi_insert(self) -> None: - wheel: WheelTimer[object] = WheelTimer(bucket_size=5) + wheel: WheelTimer[object] = WheelTimer(bucket_size=Duration(milliseconds=5)) wheel.insert(100, "1", 150) wheel.insert(105, "2", 130) @@ -54,13 +55,13 @@ def test_multi_insert(self) -> None: self.assertListEqual(wheel.fetch(210), []) def test_insert_past(self) -> None: - wheel: WheelTimer[object] = WheelTimer(bucket_size=5) + wheel: WheelTimer[object] = WheelTimer(bucket_size=Duration(milliseconds=5)) wheel.insert(100, "1", 50) self.assertListEqual(wheel.fetch(120), ["1"]) def test_insert_past_multi(self) -> None: - wheel: WheelTimer[object] = WheelTimer(bucket_size=5) + wheel: WheelTimer[object] = WheelTimer(bucket_size=Duration(milliseconds=5)) wheel.insert(100, "1", 150) wheel.insert(100, "2", 140) @@ -72,7 +73,7 @@ def test_insert_past_multi(self) -> None: self.assertListEqual(wheel.fetch(240), []) def test_multi_insert_then_past(self) -> None: - wheel: WheelTimer[object] = WheelTimer(bucket_size=5) + wheel: WheelTimer[object] = WheelTimer(bucket_size=Duration(milliseconds=5)) wheel.insert(100, "1", 150) wheel.insert(100, "2", 160) From 9de28df7a25b1f0593b70416e170b54971fefab0 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 27 Feb 2026 15:32:19 +0100 Subject: [PATCH 040/121] Bump docker/login-action from 3.6.0 to 3.7.0 in the minor-and-patches group (#19493) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps the minor-and-patches group with 1 update: [docker/login-action](https://github.com/docker/login-action). Updates `docker/login-action` from 3.6.0 to 3.7.0
Release notes

Sourced from docker/login-action's releases.

v3.7.0

Full Changelog: https://github.com/docker/login-action/compare/v3.6.0...v3.7.0

Commits
  • c94ce9f Merge pull request #915 from docker/dependabot/npm_and_yarn/lodash-4.17.23
  • 8339c95 Merge pull request #912 from docker/scope
  • c83e932 build(deps): bump lodash from 4.17.21 to 4.17.23
  • b268aa5 chore: update generated content
  • a603229 documentation for scope input
  • 7567f92 Add scope input to set scopes for the authentication token
  • 0567fa5 Merge pull request #914 from dphi/add-support-for-amazonaws.eu
  • f6ef577 feat: add support for AWS European Sovereign Cloud ECR registries
  • 916386b Merge pull request #911 from crazy-max/ensure-redact
  • 5b3f94a chore: update generated content
  • Additional commits viewable in compare view

[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=docker/login-action&package-manager=github_actions&previous-version=3.6.0&new-version=3.7.0)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores) Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`. [//]: # (dependabot-automerge-start) [//]: # (dependabot-automerge-end) ---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot show ignore conditions` will show all of the ignore conditions of the specified dependency - `@dependabot ignore major version` will close this group update PR and stop Dependabot creating any more for the specific dependency's major version (unless you unignore this specific dependency's major version or upgrade to it yourself) - `@dependabot ignore minor version` will close this group update PR and stop Dependabot creating any more for the specific dependency's minor version (unless you unignore this specific dependency's minor version or upgrade to it yourself) - `@dependabot ignore ` will close this group update PR and stop Dependabot creating any more for the specific dependency (unless you unignore this specific dependency or upgrade to it yourself) - `@dependabot unignore ` will remove all of the ignore conditions of the specified dependency - `@dependabot unignore ` will remove the ignore condition of the specified dependency and ignore conditions
Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/docker.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml index ca20e9652db..6fb6b81deb9 100644 --- a/.github/workflows/docker.yml +++ b/.github/workflows/docker.yml @@ -79,7 +79,7 @@ jobs: services/backend-repositories/secret/data/oci.element.io password | OCI_PASSWORD ; - name: Login to Element OCI Registry - uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3.6.0 + uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3.7.0 with: registry: oci-push.vpn.infra.element.io username: ${{ steps.import-secrets.outputs.OCI_USERNAME }} @@ -176,7 +176,7 @@ jobs: services/backend-repositories/secret/data/oci.element.io password | OCI_PASSWORD ; - name: Login to Element OCI Registry - uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3.6.0 + uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3.7.0 with: registry: oci-push.vpn.infra.element.io username: ${{ steps.import-secrets.outputs.OCI_USERNAME }} From b9ea2285b3cbc1e49be0d45cc46342fa6e167b28 Mon Sep 17 00:00:00 2001 From: Richard van der Hoff <1389908+richvdh@users.noreply.github.com> Date: Fri, 27 Feb 2026 14:47:07 +0000 Subject: [PATCH 041/121] Add stable support for MSC4380 invite blocking. (#19431) MSC4380 has now completed FCP, so we can add stable support for it. Co-authored-by: Quentin Gliech --- changelog.d/19431.feature | 1 + synapse/api/constants.py | 4 +- synapse/api/errors.py | 2 +- synapse/config/experimental.py | 3 -- synapse/rest/client/versions.py | 2 +- .../storage/databases/main/account_data.py | 16 +++---- tests/handlers/test_room_member.py | 43 ++++--------------- 7 files changed, 19 insertions(+), 52 deletions(-) create mode 100644 changelog.d/19431.feature diff --git a/changelog.d/19431.feature b/changelog.d/19431.feature new file mode 100644 index 00000000000..0076e5221c6 --- /dev/null +++ b/changelog.d/19431.feature @@ -0,0 +1 @@ +Add stable support for [MSC4380](https://github.com/matrix-org/matrix-spec-proposals/pull/4380) invite blocking. diff --git a/synapse/api/constants.py b/synapse/api/constants.py index b8ef5dac50f..21139a3867a 100644 --- a/synapse/api/constants.py +++ b/synapse/api/constants.py @@ -325,9 +325,7 @@ class AccountDataTypes: "org.matrix.msc4155.invite_permission_config" ) # MSC4380: Invite blocking - MSC4380_INVITE_PERMISSION_CONFIG: Final = ( - "org.matrix.msc4380.invite_permission_config" - ) + INVITE_PERMISSION_CONFIG: Final = "m.invite_permission_config" # Synapse-specific behaviour. See "Client-Server API Extensions" documentation # in Admin API for more information. SYNAPSE_ADMIN_CLIENT_CONFIG: Final = "io.element.synapse.admin_client_config" diff --git a/synapse/api/errors.py b/synapse/api/errors.py index c299ca84d94..0c35b4a7ba5 100644 --- a/synapse/api/errors.py +++ b/synapse/api/errors.py @@ -138,7 +138,7 @@ class Codes(str, Enum): KEY_TOO_LARGE = "M_KEY_TOO_LARGE" # Part of MSC4155/MSC4380 - INVITE_BLOCKED = "ORG.MATRIX.MSC4155.M_INVITE_BLOCKED" + INVITE_BLOCKED = "M_INVITE_BLOCKED" # Part of MSC4190 APPSERVICE_LOGIN_UNSUPPORTED = "IO.ELEMENT.MSC4190.M_APPSERVICE_LOGIN_UNSUPPORTED" diff --git a/synapse/config/experimental.py b/synapse/config/experimental.py index 05091ca6eb6..76e8de35fb8 100644 --- a/synapse/config/experimental.py +++ b/synapse/config/experimental.py @@ -606,6 +606,3 @@ def read_config( # Note that sticky events persisted before this feature is enabled will not be # considered sticky by the local homeserver. self.msc4354_enabled: bool = experimental.get("msc4354_enabled", False) - - # MSC4380: Invite blocking - self.msc4380_enabled: bool = experimental.get("msc4380_enabled", False) diff --git a/synapse/rest/client/versions.py b/synapse/rest/client/versions.py index e443629faba..23f5ffeedb3 100644 --- a/synapse/rest/client/versions.py +++ b/synapse/rest/client/versions.py @@ -187,7 +187,7 @@ async def on_GET(self, request: SynapseRequest) -> tuple[int, JsonDict]: # MSC4354: Sticky events "org.matrix.msc4354": self.config.experimental.msc4354_enabled, # MSC4380: Invite blocking - "org.matrix.msc4380": self.config.experimental.msc4380_enabled, + "org.matrix.msc4380.stable": True, }, }, ) diff --git a/synapse/storage/databases/main/account_data.py b/synapse/storage/databases/main/account_data.py index 71182cdab2b..538280137b0 100644 --- a/synapse/storage/databases/main/account_data.py +++ b/synapse/storage/databases/main/account_data.py @@ -109,7 +109,6 @@ def __init__( ) self._msc4155_enabled = hs.config.experimental.msc4155_enabled - self._msc4380_enabled = hs.config.experimental.msc4380_enabled def get_max_account_data_stream_id(self) -> int: """Get the current max stream ID for account data stream @@ -573,14 +572,13 @@ async def get_invite_config_for_user(self, user_id: str) -> InviteRulesConfig: Args: user_id: The user whose invite configuration should be returned. """ - if self._msc4380_enabled: - data = await self.get_global_account_data_by_type_for_user( - user_id, AccountDataTypes.MSC4380_INVITE_PERMISSION_CONFIG - ) - # If the user has an MSC4380-style config setting, prioritise that - # above an MSC4155 one - if data is not None: - return MSC4380InviteRulesConfig.from_account_data(data) + data = await self.get_global_account_data_by_type_for_user( + user_id, AccountDataTypes.INVITE_PERMISSION_CONFIG + ) + # If the user has an MSC4380-style config setting, prioritise that + # above an MSC4155 one + if data is not None: + return MSC4380InviteRulesConfig.from_account_data(data) if self._msc4155_enabled: data = await self.get_global_account_data_by_type_for_user( diff --git a/tests/handlers/test_room_member.py b/tests/handlers/test_room_member.py index d8d7caaf1bd..3890abdbc83 100644 --- a/tests/handlers/test_room_member.py +++ b/tests/handlers/test_room_member.py @@ -503,7 +503,7 @@ def test_misc4155_block_invite_local(self) -> None: SynapseError, ).value self.assertEqual(f.code, 403) - self.assertEqual(f.errcode, "ORG.MATRIX.MSC4155.M_INVITE_BLOCKED") + self.assertEqual(f.errcode, "M_INVITE_BLOCKED") @override_config({"experimental_features": {"msc4155_enabled": False}}) def test_msc4155_disabled_allow_invite_local(self) -> None: @@ -573,7 +573,7 @@ def test_msc4155_block_invite_remote(self) -> None: SynapseError, ).value self.assertEqual(f.code, 403) - self.assertEqual(f.errcode, "ORG.MATRIX.MSC4155.M_INVITE_BLOCKED") + self.assertEqual(f.errcode, "M_INVITE_BLOCKED") @override_config({"experimental_features": {"msc4155_enabled": True}}) def test_msc4155_block_invite_remote_server(self) -> None: @@ -619,7 +619,7 @@ def test_msc4155_block_invite_remote_server(self) -> None: SynapseError, ).value self.assertEqual(f.code, 403) - self.assertEqual(f.errcode, "ORG.MATRIX.MSC4155.M_INVITE_BLOCKED") + self.assertEqual(f.errcode, "M_INVITE_BLOCKED") class TestMSC4380InviteBlocking(FederatingHomeserverTestCase): @@ -642,7 +642,6 @@ def prepare(self, reactor: MemoryReactor, clock: Clock, hs: HomeServer) -> None: self.bob = self.register_user("bob", "pass") self.bob_token = self.login("bob", "pass") - @override_config({"experimental_features": {"msc4380_enabled": True}}) def test_misc4380_block_invite_local(self) -> None: """Test that MSC4380 will block a user from being invited to a room""" room_id = self.helper.create_room_as(self.alice, tok=self.alice_token) @@ -650,7 +649,7 @@ def test_misc4380_block_invite_local(self) -> None: self.get_success( self.store.add_account_data_for_user( self.bob, - AccountDataTypes.MSC4380_INVITE_PERMISSION_CONFIG, + AccountDataTypes.INVITE_PERMISSION_CONFIG, { "default_action": "block", }, @@ -667,9 +666,8 @@ def test_misc4380_block_invite_local(self) -> None: SynapseError, ).value self.assertEqual(f.code, 403) - self.assertEqual(f.errcode, "ORG.MATRIX.MSC4155.M_INVITE_BLOCKED") + self.assertEqual(f.errcode, "M_INVITE_BLOCKED") - @override_config({"experimental_features": {"msc4380_enabled": True}}) def test_misc4380_non_string_setting(self) -> None: """Test that `default_action` being set to something non-stringy is the same as "accept".""" room_id = self.helper.create_room_as(self.alice, tok=self.alice_token) @@ -677,7 +675,7 @@ def test_misc4380_non_string_setting(self) -> None: self.get_success( self.store.add_account_data_for_user( self.bob, - AccountDataTypes.MSC4380_INVITE_PERMISSION_CONFIG, + AccountDataTypes.INVITE_PERMISSION_CONFIG, { "default_action": 1, }, @@ -693,31 +691,6 @@ def test_misc4380_non_string_setting(self) -> None: ) ) - @override_config({"experimental_features": {"msc4380_enabled": False}}) - def test_msc4380_disabled_allow_invite_local(self) -> None: - """Test that, when MSC4380 is not enabled, invites are accepted as normal""" - room_id = self.helper.create_room_as(self.alice, tok=self.alice_token) - - self.get_success( - self.store.add_account_data_for_user( - self.bob, - AccountDataTypes.MSC4380_INVITE_PERMISSION_CONFIG, - { - "default_action": "block", - }, - ) - ) - - self.get_success( - self.handler.update_membership( - requester=create_requester(self.alice), - target=UserID.from_string(self.bob), - room_id=room_id, - action=Membership.INVITE, - ), - ) - - @override_config({"experimental_features": {"msc4380_enabled": True}}) def test_msc4380_block_invite_remote(self) -> None: """Test that MSC4380 will block a user from being invited to a room by a remote user.""" # A remote user who sends the invite @@ -727,7 +700,7 @@ def test_msc4380_block_invite_remote(self) -> None: self.get_success( self.store.add_account_data_for_user( self.bob, - AccountDataTypes.MSC4380_INVITE_PERMISSION_CONFIG, + AccountDataTypes.INVITE_PERMISSION_CONFIG, {"default_action": "block"}, ) ) @@ -761,4 +734,4 @@ def test_msc4380_block_invite_remote(self) -> None: SynapseError, ).value self.assertEqual(f.code, 403) - self.assertEqual(f.errcode, "ORG.MATRIX.MSC4155.M_INVITE_BLOCKED") + self.assertEqual(f.errcode, "M_INVITE_BLOCKED") From 979566ed8fa25511a1d858229c4b8848353c3167 Mon Sep 17 00:00:00 2001 From: Eric Eastwood Date: Fri, 27 Feb 2026 16:25:26 -0600 Subject: [PATCH 042/121] Pre-allocate the buffer based on the expected `Content-Length` with the Rust HTTP client (#19498) Spawning from [looking](https://matrix.to/#/!cnVVNLKqgUzNTOFQkz:matrix.org/$XOVFm5mjCzzmhUaGc202zGdSq8eWgjr00MJqNSfzHiA?via=element.io&via=matrix.org&via=one.ems.host) at some traces and seeing the Synapse Rust HTTP client taking way longer than what the Synapse Pro Event Cache claims it was able to respond in (added some [better tracing](https://github.com/element-hq/synapse-pro-modules/pull/38) for that). I don't think this specific change will have a meaningful impact but just something I saw (pre-optimization). --- changelog.d/19498.misc | 1 + rust/src/http_client.rs | 42 ++++++++++++++++++++++++++++++++++++++++- 2 files changed, 42 insertions(+), 1 deletion(-) create mode 100644 changelog.d/19498.misc diff --git a/changelog.d/19498.misc b/changelog.d/19498.misc new file mode 100644 index 00000000000..7d048c283d6 --- /dev/null +++ b/changelog.d/19498.misc @@ -0,0 +1 @@ +Pre-allocate the buffer based on the expected `Content-Length` with the Rust HTTP client. diff --git a/rust/src/http_client.rs b/rust/src/http_client.rs index 9bbdff8b454..dd37a10426b 100644 --- a/rust/src/http_client.rs +++ b/rust/src/http_client.rs @@ -16,6 +16,7 @@ use std::{collections::HashMap, future::Future, sync::OnceLock}; use anyhow::Context; use futures::TryStreamExt; +use headers::HeaderMapExt; use once_cell::sync::OnceCell; use pyo3::{create_exception, exceptions::PyException, prelude::*}; use reqwest::RequestBuilder; @@ -235,8 +236,47 @@ impl HttpClient { let status = response.status(); + // Find the expected `Content-Length` so we can pre-allocate the buffer + // necessary to read the response. It's expected that not every request will + // have a `Content-Length` header. + // + // `response.content_length()` does exist but the "value does not directly + // represents the value of the `Content-Length` header, but rather the size + // of the response’s body" + // (https://docs.rs/reqwest/latest/reqwest/struct.Response.html#method.content_length) + // and we want to avoid reading the entire body at this point because we + // purposely stream it below until the `response_limit`. + let content_length = { + let content_length = response + .headers() + .typed_get::() + // We need a `usize` for the `Vec::with_capacity(...)` usage below + .and_then(|content_length| content_length.0.try_into().ok()); + + // Sanity check that the request isn't too large from the information + // they told us (may be inaccurate so we also check below as we actually + // read the bytes) + if let Some(content_length_bytes) = content_length { + if content_length_bytes > response_limit { + Err(anyhow::anyhow!( + "Response size (defined by `Content-Length`) too large" + ))?; + } + } + + content_length + }; + + // Stream the response to avoid allocating a giant object on the server + // above our expected `response_limit`. let mut stream = response.bytes_stream(); - let mut buffer = Vec::new(); + // Pre-allocate the buffer based on the expected `Content-Length` + let mut buffer = Vec::with_capacity( + content_length + // Default to pre-allocating nothing when the request doesn't have a + // `Content-Length` header + .unwrap_or(0), + ); while let Some(chunk) = stream.try_next().await.context("reading body")? { if buffer.len() + chunk.len() > response_limit { Err(anyhow::anyhow!("Response size too large"))?; From 0d3e42f21f55e83f0cca08fe4d5c6c63834ce9b5 Mon Sep 17 00:00:00 2001 From: Erik Johnston Date: Mon, 2 Mar 2026 10:36:27 +0100 Subject: [PATCH 043/121] Yield to reactor in large loops (#19507) When a worker gets very busy some of these loops can get large and end up taking hundreds of ms to complete. To help keep the reactor tick times reasonable we add a periodic yield into these loops. These were found by doing a `py-spy` and speedscope.net (in time order) to see where we were spending blocks of time --- changelog.d/19507.misc | 1 + .../storage/databases/main/events_worker.py | 27 ++++++++++++++++--- synapse/storage/databases/main/roommember.py | 2 +- synapse/util/background_queue.py | 13 +++++++++ 4 files changed, 39 insertions(+), 4 deletions(-) create mode 100644 changelog.d/19507.misc diff --git a/changelog.d/19507.misc b/changelog.d/19507.misc new file mode 100644 index 00000000000..5a7ccff988c --- /dev/null +++ b/changelog.d/19507.misc @@ -0,0 +1 @@ +Try and reduce reactor tick times when under heavy load. diff --git a/synapse/storage/databases/main/events_worker.py b/synapse/storage/databases/main/events_worker.py index ae6ee50dc24..cb0764feb8c 100644 --- a/synapse/storage/databases/main/events_worker.py +++ b/synapse/storage/databases/main/events_worker.py @@ -132,6 +132,11 @@ def __init__( EVENT_QUEUE_TIMEOUT_S = 0.1 # Timeout when waiting for requests for events +# Number of iterations in a loop before we yield to the reactor to allow other +# things to be processed, otherwise we can end up tight looping. +ITERATIONS_BEFORE_YIELDING = 500 + + event_fetch_ongoing_gauge = Gauge( "synapse_event_fetch_ongoing", "The number of event fetchers that are running", @@ -817,7 +822,7 @@ async def get_unredacted_events_from_cache_or_db( # may be called repeatedly for the same event so at this point we cannot reach # out to any external cache for performance reasons. The external cache is # checked later on in the `get_missing_events_from_cache_or_db` function below. - event_entry_map = self._get_events_from_local_cache( + event_entry_map = await self._get_events_from_local_cache( event_ids, ) @@ -1004,7 +1009,7 @@ async def _get_events_from_cache( events: list of event_ids to fetch update_metrics: Whether to update the cache hit ratio metrics """ - event_map = self._get_events_from_local_cache( + event_map = await self._get_events_from_local_cache( events, update_metrics=update_metrics ) @@ -1045,7 +1050,7 @@ async def _get_events_from_external_cache( return event_map - def _get_events_from_local_cache( + async def _get_events_from_local_cache( self, events: Iterable[str], update_metrics: bool = True ) -> dict[str, EventCacheEntry]: """Fetch events from the local, in memory, caches. @@ -1058,7 +1063,15 @@ def _get_events_from_local_cache( """ event_map = {} + i = 0 for event_id in events: + i += 1 + + # Yield to the reactor to allow other things to be processed, + # otherwise we can end up tight looping. + if i % ITERATIONS_BEFORE_YIELDING == 0: + await self.clock.sleep(Duration(seconds=0)) + # First check if it's in the event cache ret = self._get_event_cache.get_local( (event_id,), None, update_metrics=update_metrics @@ -1375,7 +1388,15 @@ async def _fetch_event_ids_and_get_outstanding_redactions( # build a map from event_id to EventBase event_map: dict[str, EventBase] = {} + i = 0 for event_id, row in fetched_events.items(): + i += 1 + + # Yield to the reactor to allow other things to be processed, + # otherwise we can end up tight looping. + if i % ITERATIONS_BEFORE_YIELDING == 0: + await self.clock.sleep(Duration(seconds=0)) + assert row.event_id == event_id rejected_reason = row.rejected_reason diff --git a/synapse/storage/databases/main/roommember.py b/synapse/storage/databases/main/roommember.py index 7c06080f10a..736f3e4c781 100644 --- a/synapse/storage/databases/main/roommember.py +++ b/synapse/storage/databases/main/roommember.py @@ -1032,7 +1032,7 @@ async def get_joined_user_ids_from_state( # We don't update the event cache hit ratio as it completely throws off # the hit ratio counts. After all, we don't populate the cache if we # miss it here - event_map = self._get_events_from_local_cache( + event_map = await self._get_events_from_local_cache( member_event_ids, update_metrics=False ) diff --git a/synapse/util/background_queue.py b/synapse/util/background_queue.py index dfea7247f40..a86b0d0730e 100644 --- a/synapse/util/background_queue.py +++ b/synapse/util/background_queue.py @@ -37,6 +37,11 @@ T = TypeVar("T") +# Number of iterations in a loop before we yield to the reactor to allow other +# things to be processed, otherwise we can end up tight looping. +ITERATIONS_BEFORE_YIELDING = 500 + + class BackgroundQueue(Generic[T]): """A single-producer single-consumer async queue processing items in the background. @@ -65,6 +70,7 @@ def __init__( timeout_ms: int = 1000, ) -> None: self._hs = hs + self._clock = hs.get_clock() self._name = name self._callback = callback self._timeout_ms = Duration(milliseconds=timeout_ms) @@ -107,7 +113,14 @@ async def _process_queue(self) -> None: # single threaded nature, but let's be a bit defensive anyway.) self._wakeup_event.clear() + iterations = 0 while self._queue: + iterations += 1 + if iterations % ITERATIONS_BEFORE_YIELDING == 0: + # Yield to the reactor to allow other things to be processed, + # otherwise we can end up tight looping. + await self._clock.sleep(Duration(seconds=0)) + item = self._queue.popleft() try: await self._callback(item) From 825f3087bff83fc43164f14dd84b615cdb442c5c Mon Sep 17 00:00:00 2001 From: Olivier 'reivilibre Date: Mon, 2 Mar 2026 18:15:33 +0000 Subject: [PATCH 044/121] Replace deprecated collection import locations with current locations. (#19515) Use non-deprecated imports for collections Other than being deprecated, these legacy imports also don't seem to be compatible with [Ty](https://github.com/astral-sh/ty) --------- Signed-off-by: Olivier 'reivilibre --- changelog.d/19515.misc | 1 + synapse/event_auth.py | 3 ++- synapse/handlers/sliding_sync/extensions.py | 2 +- synapse/storage/controllers/stats.py | 3 ++- synapse/storage/databases/main/stats.py | 2 +- synapse/types/handlers/sliding_sync.py | 5 ++--- synapse/util/async_helpers.py | 4 ++-- synapse/util/caches/__init__.py | 4 ++-- 8 files changed, 13 insertions(+), 11 deletions(-) create mode 100644 changelog.d/19515.misc diff --git a/changelog.d/19515.misc b/changelog.d/19515.misc new file mode 100644 index 00000000000..d33af364066 --- /dev/null +++ b/changelog.d/19515.misc @@ -0,0 +1 @@ +Replace deprecated collection import locations with current locations. \ No newline at end of file diff --git a/synapse/event_auth.py b/synapse/event_auth.py index 4b354dd6fbd..7098843742e 100644 --- a/synapse/event_auth.py +++ b/synapse/event_auth.py @@ -20,12 +20,13 @@ # # +import collections import collections.abc import logging import typing +from collections import ChainMap from typing import ( Any, - ChainMap, Iterable, Mapping, MutableMapping, diff --git a/synapse/handlers/sliding_sync/extensions.py b/synapse/handlers/sliding_sync/extensions.py index d076bec51a9..9b7a01df142 100644 --- a/synapse/handlers/sliding_sync/extensions.py +++ b/synapse/handlers/sliding_sync/extensions.py @@ -14,10 +14,10 @@ import itertools import logging +from collections import ChainMap from typing import ( TYPE_CHECKING, AbstractSet, - ChainMap, Mapping, MutableMapping, Sequence, diff --git a/synapse/storage/controllers/stats.py b/synapse/storage/controllers/stats.py index 18e27e08781..5378797f046 100644 --- a/synapse/storage/controllers/stats.py +++ b/synapse/storage/controllers/stats.py @@ -20,7 +20,8 @@ # import logging -from typing import TYPE_CHECKING, Collection, Counter +from collections import Counter +from typing import TYPE_CHECKING, Collection from synapse.api.errors import SynapseError from synapse.storage.database import LoggingTransaction diff --git a/synapse/storage/databases/main/stats.py b/synapse/storage/databases/main/stats.py index 6568e2aa08c..687cabe69e3 100644 --- a/synapse/storage/databases/main/stats.py +++ b/synapse/storage/databases/main/stats.py @@ -20,12 +20,12 @@ # import logging +from collections import Counter from enum import Enum from itertools import chain from typing import ( TYPE_CHECKING, Any, - Counter, Iterable, cast, ) diff --git a/synapse/types/handlers/sliding_sync.py b/synapse/types/handlers/sliding_sync.py index dcb125c494a..694b3e1645e 100644 --- a/synapse/types/handlers/sliding_sync.py +++ b/synapse/types/handlers/sliding_sync.py @@ -13,7 +13,6 @@ # import logging -import typing from collections import ChainMap from enum import Enum from typing import ( @@ -793,7 +792,7 @@ class MutableRoomStatusMap(RoomStatusMap[T]): # We use a ChainMap here so that we can easily track what has been updated # and what hasn't. Note that when we persist the per connection state this # will get flattened to a normal dict (via calling `.copy()`) - _statuses: typing.ChainMap[str, HaveSentRoom[T]] + _statuses: ChainMap[str, HaveSentRoom[T]] def __init__( self, @@ -973,7 +972,7 @@ class MutablePerConnectionState(PerConnectionState): receipts: MutableRoomStatusMap[MultiWriterStreamToken] account_data: MutableRoomStatusMap[int] - room_configs: typing.ChainMap[str, RoomSyncConfig] + room_configs: ChainMap[str, RoomSyncConfig] # A map from room ID to the lazily-loaded memberships needed for the # request in that room. diff --git a/synapse/util/async_helpers.py b/synapse/util/async_helpers.py index 53fb24ec5b8..1c3cd48a9c0 100644 --- a/synapse/util/async_helpers.py +++ b/synapse/util/async_helpers.py @@ -25,7 +25,7 @@ import inspect import itertools import logging -import typing +from collections import OrderedDict from contextlib import asynccontextmanager from typing import ( Any, @@ -572,7 +572,7 @@ class _LinearizerEntry: # The number of things executing. count: int # Deferreds for the things blocked from executing. - deferreds: typing.OrderedDict["defer.Deferred[None]", Literal[1]] + deferreds: OrderedDict["defer.Deferred[None]", Literal[1]] class Linearizer: diff --git a/synapse/util/caches/__init__.py b/synapse/util/caches/__init__.py index a65ab7f57d8..bd52c0c0ed0 100644 --- a/synapse/util/caches/__init__.py +++ b/synapse/util/caches/__init__.py @@ -21,7 +21,7 @@ # import collections import logging -import typing +from collections import Counter from enum import Enum, auto from sys import intern from typing import Any, Callable, Sized, TypeVar @@ -134,7 +134,7 @@ class CacheMetric: hits: int = 0 misses: int = 0 - eviction_size_by_reason: typing.Counter[EvictionReason] = attr.ib( + eviction_size_by_reason: Counter[EvictionReason] = attr.ib( factory=collections.Counter ) memory_usage: int | None = None From 2deeef41187d94c34a229d1d6b1456c9c05026ae Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 3 Mar 2026 11:24:17 +0100 Subject: [PATCH 045/121] Bump futures from 0.3.31 to 0.3.32 in the patches group (#19513) Bumps the patches group with 1 update: [futures](https://github.com/rust-lang/futures-rs). Updates `futures` from 0.3.31 to 0.3.32
Release notes

Sourced from futures's releases.

0.3.32

  • Bump MSRV of utility crates to 1.71. (#2989)
  • Soft-deprecate ready! macro in favor of std::task::ready! added in Rust 1.64 (#2925)
  • Soft-deprecate pin_mut! macro in favor of std::pin::pin! added in Rust 1.68 (#2929)
  • Add FuturesOrdered::clear (#2927)
  • Add mpsc::*Receiver::recv (#2947)
  • Add mpsc::*Receiver::try_recv and deprecate mpsc::*Receiver::::try_next (#2944)
  • Implement FusedStream for sink::With (#2948)
  • Add no_std support for shared (#2868)
  • Make Mutex::new() const (#2956)
  • Add #[clippy::has_significant_drop] to guards (#2967)
  • Remove dependency to pin-utils (#2929)
  • Remove dependency on num_cpus (#2946)
  • Performance improvements (#2983)
  • Documentation improvements (#2925, #2926, #2940, #2971)
Changelog

Sourced from futures's changelog.

0.3.32 - 2026-02-15

  • Bump MSRV of utility crates to 1.71. (#2989)
  • Soft-deprecate ready! macro in favor of std::task::ready! added in Rust 1.64 (#2925)
  • Soft-deprecate pin_mut! macro in favor of std::pin::pin! added in Rust 1.68 (#2929)
  • Add FuturesOrdered::clear (#2927)
  • Add mpsc::*Receiver::recv (#2947)
  • Add mpsc::*Receiver::try_recv and deprecate mpsc::*Receiver::::try_next (#2944)
  • Implement FusedStream for sink::With (#2948)
  • Add no_std support for shared (#2868)
  • Make Mutex::new() const (#2956)
  • Add #[clippy::has_significant_drop] to guards (#2967)
  • Remove dependency to pin-utils (#2929)
  • Remove dependency on num_cpus (#2946)
  • Performance improvements (#2983)
  • Documentation improvements (#2925, #2926, #2940, #2971)
Commits
  • d9bba94 Release 0.3.32
  • 151e0b9 Add comments on rust-version field in Cargo.toml
  • 4aaf00c Bump MSRV of utility crates to 1.71
  • a4cce12 perf: improve AtomicWaker::wake performance (#2983)
  • ba9d102 Add #[clippy::has_significant_drop] to guards (#2967)
  • 20396a8 Fix rustdoc::broken_intra_doc_links warning
  • 815f6eb Fix documentation of BiLock::lock (#2971)
  • 0f0db04 futures-util: make Mutex::new() const (#2956)
  • 5d6fc5e ci: Test big-endian target (s390x Linux)
  • 9f739fe Ignore dead_code lint on Fn1 trait
  • Additional commits viewable in compare view

[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=futures&package-manager=cargo&previous-version=0.3.31&new-version=0.3.32)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores) Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`. [//]: # (dependabot-automerge-start) [//]: # (dependabot-automerge-end) ---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot show ignore conditions` will show all of the ignore conditions of the specified dependency - `@dependabot ignore major version` will close this group update PR and stop Dependabot creating any more for the specific dependency's major version (unless you unignore this specific dependency's major version or upgrade to it yourself) - `@dependabot ignore minor version` will close this group update PR and stop Dependabot creating any more for the specific dependency's minor version (unless you unignore this specific dependency's minor version or upgrade to it yourself) - `@dependabot ignore ` will close this group update PR and stop Dependabot creating any more for the specific dependency (unless you unignore this specific dependency or upgrade to it yourself) - `@dependabot unignore ` will remove all of the ignore conditions of the specified dependency - `@dependabot unignore ` will remove the ignore condition of the specified dependency and ignore conditions
Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- Cargo.lock | 43 ++++++++++++++++++------------------------- 1 file changed, 18 insertions(+), 25 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 15342020c90..c43ee29c76e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -187,9 +187,9 @@ dependencies = [ [[package]] name = "futures" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "65bc07b1a8bc7c85c5f2e110c476c7389b4554ba72af57d8445ea63a576b0876" +checksum = "8b147ee9d1f6d097cef9ce628cd2ee62288d963e16fb287bd9286455b241382d" dependencies = [ "futures-channel", "futures-core", @@ -202,9 +202,9 @@ dependencies = [ [[package]] name = "futures-channel" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10" +checksum = "07bbe89c50d7a535e539b8c17bc0b49bdb77747034daa8087407d655f3f7cc1d" dependencies = [ "futures-core", "futures-sink", @@ -212,15 +212,15 @@ dependencies = [ [[package]] name = "futures-core" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" +checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d" [[package]] name = "futures-executor" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e28d1d997f585e54aebc3f97d39e72338912123a67330d723fdbb564d646c9f" +checksum = "baf29c38818342a3b26b5b923639e7b1f4a61fc5e76102d4b1981c6dc7a7579d" dependencies = [ "futures-core", "futures-task", @@ -229,15 +229,15 @@ dependencies = [ [[package]] name = "futures-io" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" +checksum = "cecba35d7ad927e23624b22ad55235f2239cfa44fd10428eecbeba6d6a717718" [[package]] name = "futures-macro" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" +checksum = "e835b70203e41293343137df5c0664546da5745f82ec9b84d40be8336958447b" dependencies = [ "proc-macro2", "quote", @@ -246,21 +246,21 @@ dependencies = [ [[package]] name = "futures-sink" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7" +checksum = "c39754e157331b013978ec91992bde1ac089843443c49cbc7f46150b0fad0893" [[package]] name = "futures-task" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988" +checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393" [[package]] name = "futures-util" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" +checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6" dependencies = [ "futures-channel", "futures-core", @@ -270,7 +270,6 @@ dependencies = [ "futures-task", "memchr", "pin-project-lite", - "pin-utils", "slab", ] @@ -771,12 +770,6 @@ version = "0.2.16" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" -[[package]] -name = "pin-utils" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" - [[package]] name = "portable-atomic" version = "1.11.1" From 094a48efb5ff33821ed6a41cbe0bf6db7d91b8d8 Mon Sep 17 00:00:00 2001 From: Quentin Gliech Date: Tue, 3 Mar 2026 14:29:59 +0100 Subject: [PATCH 046/121] Bump all locked dependencies to their latest versions. (#19519) This is a manual lock bump, as it looks like Dependabot is currently timing out updating dependencies. This should hopefully unlock it, as it will have fewer dependencies to update. Two outstanding exceptions: - pympler upgrade adds a pywin32 deps, which is missing sdist (so CI is complaining) - pysaml2 for some unknown reason pinned the MAX version of pyopenssl, which duplicates pyopenssl and cryptography, which obviously breaks stuff --- changelog.d/19519.misc | 1 + poetry.lock | 1610 +++++++++++++++++++++++----------------- 2 files changed, 914 insertions(+), 697 deletions(-) create mode 100644 changelog.d/19519.misc diff --git a/changelog.d/19519.misc b/changelog.d/19519.misc new file mode 100644 index 00000000000..860a5f5130d --- /dev/null +++ b/changelog.d/19519.misc @@ -0,0 +1 @@ +Bump most locked Python dependencies to their latest versions. diff --git a/poetry.lock b/poetry.lock index 3c80a4dfdc5..7f9b34d38f8 100644 --- a/poetry.lock +++ b/poetry.lock @@ -26,15 +26,15 @@ files = [ [[package]] name = "authlib" -version = "1.6.6" +version = "1.6.9" description = "The ultimate Python library in building OAuth and OpenID Connect servers and clients." optional = true python-versions = ">=3.9" groups = ["main"] -markers = "extra == \"all\" or extra == \"jwt\" or extra == \"oidc\"" +markers = "extra == \"oidc\" or extra == \"jwt\" or extra == \"all\"" files = [ - {file = "authlib-1.6.6-py2.py3-none-any.whl", hash = "sha256:7d9e9bc535c13974313a87f53e8430eb6ea3d1cf6ae4f6efcd793f2e949143fd"}, - {file = "authlib-1.6.6.tar.gz", hash = "sha256:45770e8e056d0f283451d9996fbb59b70d45722b45d854d58f32878d0a40c38e"}, + {file = "authlib-1.6.9-py2.py3-none-any.whl", hash = "sha256:f08b4c14e08f0861dc18a32357b33fbcfd2ea86cfe3fe149484b4d764c4a0ac3"}, + {file = "authlib-1.6.9.tar.gz", hash = "sha256:d8f2421e7e5980cc1ddb4e32d3f5fa659cfaf60d8eaf3281ebed192e4ab74f04"}, ] [package.dependencies] @@ -55,6 +55,23 @@ files = [ [package.extras] visualize = ["Twisted (>=16.1.1)", "graphviz (>0.5.1)"] +[[package]] +name = "backports-tarfile" +version = "1.2.0" +description = "Backport of CPython tarfile module" +optional = false +python-versions = ">=3.8" +groups = ["dev"] +markers = "python_version < \"3.12\" and platform_machine != \"ppc64le\" and platform_machine != \"s390x\"" +files = [ + {file = "backports.tarfile-1.2.0-py3-none-any.whl", hash = "sha256:77e284d754527b01fb1e6fa8a1afe577858ebe4e9dad8919e34c862cb399bc34"}, + {file = "backports_tarfile-1.2.0.tar.gz", hash = "sha256:d75e02c268746e1b8144c278978b6e98e85de6ad16f8e4b0844a154557eca991"}, +] + +[package.extras] +docs = ["furo", "jaraco.packaging (>=9.3)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] +testing = ["jaraco.test", "pytest (!=8.0.*)", "pytest (>=6,!=8.1.*)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)"] + [[package]] name = "bcrypt" version = "5.0.0" @@ -138,7 +155,7 @@ version = "6.3.0" description = "An easy safelist-based HTML-sanitizing tool." optional = false python-versions = ">=3.10" -groups = ["main", "dev"] +groups = ["main"] files = [ {file = "bleach-6.3.0-py3-none-any.whl", hash = "sha256:fe10ec77c93ddf3d13a73b035abaac7a9f5e436513864ccdad516693213c65d6"}, {file = "bleach-6.3.0.tar.gz", hash = "sha256:6f3b91b1c0a02bb9a78b5a454c92506aa0fdf197e1d5e114d2e00c6f64306d22"}, @@ -164,14 +181,14 @@ files = [ [[package]] name = "certifi" -version = "2024.7.4" +version = "2026.2.25" description = "Python package for providing Mozilla's CA Bundle." optional = false -python-versions = ">=3.6" +python-versions = ">=3.7" groups = ["main", "dev"] files = [ - {file = "certifi-2024.7.4-py3-none-any.whl", hash = "sha256:c198e21b1289c2ab85ee4e67bb4b4ef3ead0892059901a8d5b622f24a1101e90"}, - {file = "certifi-2024.7.4.tar.gz", hash = "sha256:5a1e7645bc0ec61a09e26c36f6106dd4cf40c6db3a1fb6352b0244e7fb057c7b"}, + {file = "certifi-2026.2.25-py3-none-any.whl", hash = "sha256:027692e4402ad994f1c42e52a4997a9763c646b73e4096e4d5d6db8af1d6f0fa"}, + {file = "certifi-2026.2.25.tar.gz", hash = "sha256:e887ab5cee78ea814d3472169153c2d12cd43b14bd03329a39a9c6e2e80bfba7"}, ] [[package]] @@ -273,87 +290,125 @@ pycparser = {version = "*", markers = "implementation_name != \"PyPy\""} [[package]] name = "charset-normalizer" -version = "3.1.0" +version = "3.4.4" description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet." optional = false -python-versions = ">=3.7.0" +python-versions = ">=3.7" groups = ["main", "dev"] files = [ - {file = "charset-normalizer-3.1.0.tar.gz", hash = "sha256:34e0a2f9c370eb95597aae63bf85eb5e96826d81e3dcf88b8886012906f509b5"}, - {file = "charset_normalizer-3.1.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:e0ac8959c929593fee38da1c2b64ee9778733cdf03c482c9ff1d508b6b593b2b"}, - {file = "charset_normalizer-3.1.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d7fc3fca01da18fbabe4625d64bb612b533533ed10045a2ac3dd194bfa656b60"}, - {file = "charset_normalizer-3.1.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:04eefcee095f58eaabe6dc3cc2262f3bcd776d2c67005880894f447b3f2cb9c1"}, - {file = "charset_normalizer-3.1.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:20064ead0717cf9a73a6d1e779b23d149b53daf971169289ed2ed43a71e8d3b0"}, - {file = "charset_normalizer-3.1.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1435ae15108b1cb6fffbcea2af3d468683b7afed0169ad718451f8db5d1aff6f"}, - {file = "charset_normalizer-3.1.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c84132a54c750fda57729d1e2599bb598f5fa0344085dbde5003ba429a4798c0"}, - {file = "charset_normalizer-3.1.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:75f2568b4189dda1c567339b48cba4ac7384accb9c2a7ed655cd86b04055c795"}, - {file = "charset_normalizer-3.1.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:11d3bcb7be35e7b1bba2c23beedac81ee893ac9871d0ba79effc7fc01167db6c"}, - {file = "charset_normalizer-3.1.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:891cf9b48776b5c61c700b55a598621fdb7b1e301a550365571e9624f270c203"}, - {file = "charset_normalizer-3.1.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:5f008525e02908b20e04707a4f704cd286d94718f48bb33edddc7d7b584dddc1"}, - {file = "charset_normalizer-3.1.0-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:b06f0d3bf045158d2fb8837c5785fe9ff9b8c93358be64461a1089f5da983137"}, - {file = "charset_normalizer-3.1.0-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:49919f8400b5e49e961f320c735388ee686a62327e773fa5b3ce6721f7e785ce"}, - {file = "charset_normalizer-3.1.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:22908891a380d50738e1f978667536f6c6b526a2064156203d418f4856d6e86a"}, - {file = "charset_normalizer-3.1.0-cp310-cp310-win32.whl", hash = "sha256:12d1a39aa6b8c6f6248bb54550efcc1c38ce0d8096a146638fd4738e42284448"}, - {file = "charset_normalizer-3.1.0-cp310-cp310-win_amd64.whl", hash = "sha256:65ed923f84a6844de5fd29726b888e58c62820e0769b76565480e1fdc3d062f8"}, - {file = "charset_normalizer-3.1.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:9a3267620866c9d17b959a84dd0bd2d45719b817245e49371ead79ed4f710d19"}, - {file = "charset_normalizer-3.1.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:6734e606355834f13445b6adc38b53c0fd45f1a56a9ba06c2058f86893ae8017"}, - {file = "charset_normalizer-3.1.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:f8303414c7b03f794347ad062c0516cee0e15f7a612abd0ce1e25caf6ceb47df"}, - {file = "charset_normalizer-3.1.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:aaf53a6cebad0eae578f062c7d462155eada9c172bd8c4d250b8c1d8eb7f916a"}, - {file = "charset_normalizer-3.1.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3dc5b6a8ecfdc5748a7e429782598e4f17ef378e3e272eeb1340ea57c9109f41"}, - {file = "charset_normalizer-3.1.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e1b25e3ad6c909f398df8921780d6a3d120d8c09466720226fc621605b6f92b1"}, - {file = "charset_normalizer-3.1.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0ca564606d2caafb0abe6d1b5311c2649e8071eb241b2d64e75a0d0065107e62"}, - {file = "charset_normalizer-3.1.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b82fab78e0b1329e183a65260581de4375f619167478dddab510c6c6fb04d9b6"}, - {file = "charset_normalizer-3.1.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:bd7163182133c0c7701b25e604cf1611c0d87712e56e88e7ee5d72deab3e76b5"}, - {file = "charset_normalizer-3.1.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:11d117e6c63e8f495412d37e7dc2e2fff09c34b2d09dbe2bee3c6229577818be"}, - {file = "charset_normalizer-3.1.0-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:cf6511efa4801b9b38dc5546d7547d5b5c6ef4b081c60b23e4d941d0eba9cbeb"}, - {file = "charset_normalizer-3.1.0-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:abc1185d79f47c0a7aaf7e2412a0eb2c03b724581139193d2d82b3ad8cbb00ac"}, - {file = "charset_normalizer-3.1.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:cb7b2ab0188829593b9de646545175547a70d9a6e2b63bf2cd87a0a391599324"}, - {file = "charset_normalizer-3.1.0-cp311-cp311-win32.whl", hash = "sha256:c36bcbc0d5174a80d6cccf43a0ecaca44e81d25be4b7f90f0ed7bcfbb5a00909"}, - {file = "charset_normalizer-3.1.0-cp311-cp311-win_amd64.whl", hash = "sha256:cca4def576f47a09a943666b8f829606bcb17e2bc2d5911a46c8f8da45f56755"}, - {file = "charset_normalizer-3.1.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:0c95f12b74681e9ae127728f7e5409cbbef9cd914d5896ef238cc779b8152373"}, - {file = "charset_normalizer-3.1.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fca62a8301b605b954ad2e9c3666f9d97f63872aa4efcae5492baca2056b74ab"}, - {file = "charset_normalizer-3.1.0-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ac0aa6cd53ab9a31d397f8303f92c42f534693528fafbdb997c82bae6e477ad9"}, - {file = "charset_normalizer-3.1.0-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c3af8e0f07399d3176b179f2e2634c3ce9c1301379a6b8c9c9aeecd481da494f"}, - {file = "charset_normalizer-3.1.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3a5fc78f9e3f501a1614a98f7c54d3969f3ad9bba8ba3d9b438c3bc5d047dd28"}, - {file = "charset_normalizer-3.1.0-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:628c985afb2c7d27a4800bfb609e03985aaecb42f955049957814e0491d4006d"}, - {file = "charset_normalizer-3.1.0-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:74db0052d985cf37fa111828d0dd230776ac99c740e1a758ad99094be4f1803d"}, - {file = "charset_normalizer-3.1.0-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:1e8fcdd8f672a1c4fc8d0bd3a2b576b152d2a349782d1eb0f6b8e52e9954731d"}, - {file = "charset_normalizer-3.1.0-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:04afa6387e2b282cf78ff3dbce20f0cc071c12dc8f685bd40960cc68644cfea6"}, - {file = "charset_normalizer-3.1.0-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:dd5653e67b149503c68c4018bf07e42eeed6b4e956b24c00ccdf93ac79cdff84"}, - {file = "charset_normalizer-3.1.0-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:d2686f91611f9e17f4548dbf050e75b079bbc2a82be565832bc8ea9047b61c8c"}, - {file = "charset_normalizer-3.1.0-cp37-cp37m-win32.whl", hash = "sha256:4155b51ae05ed47199dc5b2a4e62abccb274cee6b01da5b895099b61b1982974"}, - {file = "charset_normalizer-3.1.0-cp37-cp37m-win_amd64.whl", hash = "sha256:322102cdf1ab682ecc7d9b1c5eed4ec59657a65e1c146a0da342b78f4112db23"}, - {file = "charset_normalizer-3.1.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:e633940f28c1e913615fd624fcdd72fdba807bf53ea6925d6a588e84e1151531"}, - {file = "charset_normalizer-3.1.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:3a06f32c9634a8705f4ca9946d667609f52cf130d5548881401f1eb2c39b1e2c"}, - {file = "charset_normalizer-3.1.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:7381c66e0561c5757ffe616af869b916c8b4e42b367ab29fedc98481d1e74e14"}, - {file = "charset_normalizer-3.1.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3573d376454d956553c356df45bb824262c397c6e26ce43e8203c4c540ee0acb"}, - {file = "charset_normalizer-3.1.0-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e89df2958e5159b811af9ff0f92614dabf4ff617c03a4c1c6ff53bf1c399e0e1"}, - {file = "charset_normalizer-3.1.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:78cacd03e79d009d95635e7d6ff12c21eb89b894c354bd2b2ed0b4763373693b"}, - {file = "charset_normalizer-3.1.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:de5695a6f1d8340b12a5d6d4484290ee74d61e467c39ff03b39e30df62cf83a0"}, - {file = "charset_normalizer-3.1.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1c60b9c202d00052183c9be85e5eaf18a4ada0a47d188a83c8f5c5b23252f649"}, - {file = "charset_normalizer-3.1.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:f645caaf0008bacf349875a974220f1f1da349c5dbe7c4ec93048cdc785a3326"}, - {file = "charset_normalizer-3.1.0-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:ea9f9c6034ea2d93d9147818f17c2a0860d41b71c38b9ce4d55f21b6f9165a11"}, - {file = "charset_normalizer-3.1.0-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:80d1543d58bd3d6c271b66abf454d437a438dff01c3e62fdbcd68f2a11310d4b"}, - {file = "charset_normalizer-3.1.0-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:73dc03a6a7e30b7edc5b01b601e53e7fc924b04e1835e8e407c12c037e81adbd"}, - {file = "charset_normalizer-3.1.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:6f5c2e7bc8a4bf7c426599765b1bd33217ec84023033672c1e9a8b35eaeaaaf8"}, - {file = "charset_normalizer-3.1.0-cp38-cp38-win32.whl", hash = "sha256:12a2b561af122e3d94cdb97fe6fb2bb2b82cef0cdca131646fdb940a1eda04f0"}, - {file = "charset_normalizer-3.1.0-cp38-cp38-win_amd64.whl", hash = "sha256:3160a0fd9754aab7d47f95a6b63ab355388d890163eb03b2d2b87ab0a30cfa59"}, - {file = "charset_normalizer-3.1.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:38e812a197bf8e71a59fe55b757a84c1f946d0ac114acafaafaf21667a7e169e"}, - {file = "charset_normalizer-3.1.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:6baf0baf0d5d265fa7944feb9f7451cc316bfe30e8df1a61b1bb08577c554f31"}, - {file = "charset_normalizer-3.1.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:8f25e17ab3039b05f762b0a55ae0b3632b2e073d9c8fc88e89aca31a6198e88f"}, - {file = "charset_normalizer-3.1.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3747443b6a904001473370d7810aa19c3a180ccd52a7157aacc264a5ac79265e"}, - {file = "charset_normalizer-3.1.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b116502087ce8a6b7a5f1814568ccbd0e9f6cfd99948aa59b0e241dc57cf739f"}, - {file = "charset_normalizer-3.1.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d16fd5252f883eb074ca55cb622bc0bee49b979ae4e8639fff6ca3ff44f9f854"}, - {file = "charset_normalizer-3.1.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:21fa558996782fc226b529fdd2ed7866c2c6ec91cee82735c98a197fae39f706"}, - {file = "charset_normalizer-3.1.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6f6c7a8a57e9405cad7485f4c9d3172ae486cfef1344b5ddd8e5239582d7355e"}, - {file = "charset_normalizer-3.1.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:ac3775e3311661d4adace3697a52ac0bab17edd166087d493b52d4f4f553f9f0"}, - {file = "charset_normalizer-3.1.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:10c93628d7497c81686e8e5e557aafa78f230cd9e77dd0c40032ef90c18f2230"}, - {file = "charset_normalizer-3.1.0-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:6f4f4668e1831850ebcc2fd0b1cd11721947b6dc7c00bf1c6bd3c929ae14f2c7"}, - {file = "charset_normalizer-3.1.0-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:0be65ccf618c1e7ac9b849c315cc2e8a8751d9cfdaa43027d4f6624bd587ab7e"}, - {file = "charset_normalizer-3.1.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:53d0a3fa5f8af98a1e261de6a3943ca631c526635eb5817a87a59d9a57ebf48f"}, - {file = "charset_normalizer-3.1.0-cp39-cp39-win32.whl", hash = "sha256:a04f86f41a8916fe45ac5024ec477f41f886b3c435da2d4e3d2709b22ab02af1"}, - {file = "charset_normalizer-3.1.0-cp39-cp39-win_amd64.whl", hash = "sha256:830d2948a5ec37c386d3170c483063798d7879037492540f10a475e3fd6f244b"}, - {file = "charset_normalizer-3.1.0-py3-none-any.whl", hash = "sha256:3d9098b479e78c85080c98e1e35ff40b4a31d8953102bb0fd7d1b6f8a2111a3d"}, + {file = "charset_normalizer-3.4.4-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:e824f1492727fa856dd6eda4f7cee25f8518a12f3c4a56a74e8095695089cf6d"}, + {file = "charset_normalizer-3.4.4-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4bd5d4137d500351a30687c2d3971758aac9a19208fc110ccb9d7188fbe709e8"}, + {file = "charset_normalizer-3.4.4-cp310-cp310-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:027f6de494925c0ab2a55eab46ae5129951638a49a34d87f4c3eda90f696b4ad"}, + {file = "charset_normalizer-3.4.4-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f820802628d2694cb7e56db99213f930856014862f3fd943d290ea8438d07ca8"}, + {file = "charset_normalizer-3.4.4-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:798d75d81754988d2565bff1b97ba5a44411867c0cf32b77a7e8f8d84796b10d"}, + {file = "charset_normalizer-3.4.4-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9d1bb833febdff5c8927f922386db610b49db6e0d4f4ee29601d71e7c2694313"}, + {file = "charset_normalizer-3.4.4-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:9cd98cdc06614a2f768d2b7286d66805f94c48cde050acdbbb7db2600ab3197e"}, + {file = "charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:077fbb858e903c73f6c9db43374fd213b0b6a778106bc7032446a8e8b5b38b93"}, + {file = "charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:244bfb999c71b35de57821b8ea746b24e863398194a4014e4c76adc2bbdfeff0"}, + {file = "charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:64b55f9dce520635f018f907ff1b0df1fdc31f2795a922fb49dd14fbcdf48c84"}, + {file = "charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:faa3a41b2b66b6e50f84ae4a68c64fcd0c44355741c6374813a800cd6695db9e"}, + {file = "charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:6515f3182dbe4ea06ced2d9e8666d97b46ef4c75e326b79bb624110f122551db"}, + {file = "charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:cc00f04ed596e9dc0da42ed17ac5e596c6ccba999ba6bd92b0e0aef2f170f2d6"}, + {file = "charset_normalizer-3.4.4-cp310-cp310-win32.whl", hash = "sha256:f34be2938726fc13801220747472850852fe6b1ea75869a048d6f896838c896f"}, + {file = "charset_normalizer-3.4.4-cp310-cp310-win_amd64.whl", hash = "sha256:a61900df84c667873b292c3de315a786dd8dac506704dea57bc957bd31e22c7d"}, + {file = "charset_normalizer-3.4.4-cp310-cp310-win_arm64.whl", hash = "sha256:cead0978fc57397645f12578bfd2d5ea9138ea0fac82b2f63f7f7c6877986a69"}, + {file = "charset_normalizer-3.4.4-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:6e1fcf0720908f200cd21aa4e6750a48ff6ce4afe7ff5a79a90d5ed8a08296f8"}, + {file = "charset_normalizer-3.4.4-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5f819d5fe9234f9f82d75bdfa9aef3a3d72c4d24a6e57aeaebba32a704553aa0"}, + {file = "charset_normalizer-3.4.4-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:a59cb51917aa591b1c4e6a43c132f0cdc3c76dbad6155df4e28ee626cc77a0a3"}, + {file = "charset_normalizer-3.4.4-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:8ef3c867360f88ac904fd3f5e1f902f13307af9052646963ee08ff4f131adafc"}, + {file = "charset_normalizer-3.4.4-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d9e45d7faa48ee908174d8fe84854479ef838fc6a705c9315372eacbc2f02897"}, + {file = "charset_normalizer-3.4.4-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:840c25fb618a231545cbab0564a799f101b63b9901f2569faecd6b222ac72381"}, + {file = "charset_normalizer-3.4.4-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:ca5862d5b3928c4940729dacc329aa9102900382fea192fc5e52eb69d6093815"}, + {file = "charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d9c7f57c3d666a53421049053eaacdd14bbd0a528e2186fcb2e672effd053bb0"}, + {file = "charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:277e970e750505ed74c832b4bf75dac7476262ee2a013f5574dd49075879e161"}, + {file = "charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:31fd66405eaf47bb62e8cd575dc621c56c668f27d46a61d975a249930dd5e2a4"}, + {file = "charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:0d3d8f15c07f86e9ff82319b3d9ef6f4bf907608f53fe9d92b28ea9ae3d1fd89"}, + {file = "charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:9f7fcd74d410a36883701fafa2482a6af2ff5ba96b9a620e9e0721e28ead5569"}, + {file = "charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ebf3e58c7ec8a8bed6d66a75d7fb37b55e5015b03ceae72a8e7c74495551e224"}, + {file = "charset_normalizer-3.4.4-cp311-cp311-win32.whl", hash = "sha256:eecbc200c7fd5ddb9a7f16c7decb07b566c29fa2161a16cf67b8d068bd21690a"}, + {file = "charset_normalizer-3.4.4-cp311-cp311-win_amd64.whl", hash = "sha256:5ae497466c7901d54b639cf42d5b8c1b6a4fead55215500d2f486d34db48d016"}, + {file = "charset_normalizer-3.4.4-cp311-cp311-win_arm64.whl", hash = "sha256:65e2befcd84bc6f37095f5961e68a6f077bf44946771354a28ad434c2cce0ae1"}, + {file = "charset_normalizer-3.4.4-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:0a98e6759f854bd25a58a73fa88833fba3b7c491169f86ce1180c948ab3fd394"}, + {file = "charset_normalizer-3.4.4-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b5b290ccc2a263e8d185130284f8501e3e36c5e02750fc6b6bdeb2e9e96f1e25"}, + {file = "charset_normalizer-3.4.4-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:74bb723680f9f7a6234dcf67aea57e708ec1fbdf5699fb91dfd6f511b0a320ef"}, + {file = "charset_normalizer-3.4.4-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f1e34719c6ed0b92f418c7c780480b26b5d9c50349e9a9af7d76bf757530350d"}, + {file = "charset_normalizer-3.4.4-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:2437418e20515acec67d86e12bf70056a33abdacb5cb1655042f6538d6b085a8"}, + {file = "charset_normalizer-3.4.4-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:11d694519d7f29d6cd09f6ac70028dba10f92f6cdd059096db198c283794ac86"}, + {file = "charset_normalizer-3.4.4-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:ac1c4a689edcc530fc9d9aa11f5774b9e2f33f9a0c6a57864e90908f5208d30a"}, + {file = "charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:21d142cc6c0ec30d2efee5068ca36c128a30b0f2c53c1c07bd78cb6bc1d3be5f"}, + {file = "charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:5dbe56a36425d26d6cfb40ce79c314a2e4dd6211d51d6d2191c00bed34f354cc"}, + {file = "charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:5bfbb1b9acf3334612667b61bd3002196fe2a1eb4dd74d247e0f2a4d50ec9bbf"}, + {file = "charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:d055ec1e26e441f6187acf818b73564e6e6282709e9bcb5b63f5b23068356a15"}, + {file = "charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:af2d8c67d8e573d6de5bc30cdb27e9b95e49115cd9baad5ddbd1a6207aaa82a9"}, + {file = "charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:780236ac706e66881f3b7f2f32dfe90507a09e67d1d454c762cf642e6e1586e0"}, + {file = "charset_normalizer-3.4.4-cp312-cp312-win32.whl", hash = "sha256:5833d2c39d8896e4e19b689ffc198f08ea58116bee26dea51e362ecc7cd3ed26"}, + {file = "charset_normalizer-3.4.4-cp312-cp312-win_amd64.whl", hash = "sha256:a79cfe37875f822425b89a82333404539ae63dbdddf97f84dcbc3d339aae9525"}, + {file = "charset_normalizer-3.4.4-cp312-cp312-win_arm64.whl", hash = "sha256:376bec83a63b8021bb5c8ea75e21c4ccb86e7e45ca4eb81146091b56599b80c3"}, + {file = "charset_normalizer-3.4.4-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:e1f185f86a6f3403aa2420e815904c67b2f9ebc443f045edd0de921108345794"}, + {file = "charset_normalizer-3.4.4-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6b39f987ae8ccdf0d2642338faf2abb1862340facc796048b604ef14919e55ed"}, + {file = "charset_normalizer-3.4.4-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:3162d5d8ce1bb98dd51af660f2121c55d0fa541b46dff7bb9b9f86ea1d87de72"}, + {file = "charset_normalizer-3.4.4-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:81d5eb2a312700f4ecaa977a8235b634ce853200e828fbadf3a9c50bab278328"}, + {file = "charset_normalizer-3.4.4-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5bd2293095d766545ec1a8f612559f6b40abc0eb18bb2f5d1171872d34036ede"}, + {file = "charset_normalizer-3.4.4-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a8a8b89589086a25749f471e6a900d3f662d1d3b6e2e59dcecf787b1cc3a1894"}, + {file = "charset_normalizer-3.4.4-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:bc7637e2f80d8530ee4a78e878bce464f70087ce73cf7c1caf142416923b98f1"}, + {file = "charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f8bf04158c6b607d747e93949aa60618b61312fe647a6369f88ce2ff16043490"}, + {file = "charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:554af85e960429cf30784dd47447d5125aaa3b99a6f0683589dbd27e2f45da44"}, + {file = "charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:74018750915ee7ad843a774364e13a3db91682f26142baddf775342c3f5b1133"}, + {file = "charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:c0463276121fdee9c49b98908b3a89c39be45d86d1dbaa22957e38f6321d4ce3"}, + {file = "charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:362d61fd13843997c1c446760ef36f240cf81d3ebf74ac62652aebaf7838561e"}, + {file = "charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9a26f18905b8dd5d685d6d07b0cdf98a79f3c7a918906af7cc143ea2e164c8bc"}, + {file = "charset_normalizer-3.4.4-cp313-cp313-win32.whl", hash = "sha256:9b35f4c90079ff2e2edc5b26c0c77925e5d2d255c42c74fdb70fb49b172726ac"}, + {file = "charset_normalizer-3.4.4-cp313-cp313-win_amd64.whl", hash = "sha256:b435cba5f4f750aa6c0a0d92c541fb79f69a387c91e61f1795227e4ed9cece14"}, + {file = "charset_normalizer-3.4.4-cp313-cp313-win_arm64.whl", hash = "sha256:542d2cee80be6f80247095cc36c418f7bddd14f4a6de45af91dfad36d817bba2"}, + {file = "charset_normalizer-3.4.4-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:da3326d9e65ef63a817ecbcc0df6e94463713b754fe293eaa03da99befb9a5bd"}, + {file = "charset_normalizer-3.4.4-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8af65f14dc14a79b924524b1e7fffe304517b2bff5a58bf64f30b98bbc5079eb"}, + {file = "charset_normalizer-3.4.4-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:74664978bb272435107de04e36db5a9735e78232b85b77d45cfb38f758efd33e"}, + {file = "charset_normalizer-3.4.4-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:752944c7ffbfdd10c074dc58ec2d5a8a4cd9493b314d367c14d24c17684ddd14"}, + {file = "charset_normalizer-3.4.4-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d1f13550535ad8cff21b8d757a3257963e951d96e20ec82ab44bc64aeb62a191"}, + {file = "charset_normalizer-3.4.4-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ecaae4149d99b1c9e7b88bb03e3221956f68fd6d50be2ef061b2381b61d20838"}, + {file = "charset_normalizer-3.4.4-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:cb6254dc36b47a990e59e1068afacdcd02958bdcce30bb50cc1700a8b9d624a6"}, + {file = "charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:c8ae8a0f02f57a6e61203a31428fa1d677cbe50c93622b4149d5c0f319c1d19e"}, + {file = "charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:47cc91b2f4dd2833fddaedd2893006b0106129d4b94fdb6af1f4ce5a9965577c"}, + {file = "charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:82004af6c302b5d3ab2cfc4cc5f29db16123b1a8417f2e25f9066f91d4411090"}, + {file = "charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:2b7d8f6c26245217bd2ad053761201e9f9680f8ce52f0fcd8d0755aeae5b2152"}, + {file = "charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:799a7a5e4fb2d5898c60b640fd4981d6a25f1c11790935a44ce38c54e985f828"}, + {file = "charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:99ae2cffebb06e6c22bdc25801d7b30f503cc87dbd283479e7b606f70aff57ec"}, + {file = "charset_normalizer-3.4.4-cp314-cp314-win32.whl", hash = "sha256:f9d332f8c2a2fcbffe1378594431458ddbef721c1769d78e2cbc06280d8155f9"}, + {file = "charset_normalizer-3.4.4-cp314-cp314-win_amd64.whl", hash = "sha256:8a6562c3700cce886c5be75ade4a5db4214fda19fede41d9792d100288d8f94c"}, + {file = "charset_normalizer-3.4.4-cp314-cp314-win_arm64.whl", hash = "sha256:de00632ca48df9daf77a2c65a484531649261ec9f25489917f09e455cb09ddb2"}, + {file = "charset_normalizer-3.4.4-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:ce8a0633f41a967713a59c4139d29110c07e826d131a316b50ce11b1d79b4f84"}, + {file = "charset_normalizer-3.4.4-cp38-cp38-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:eaabd426fe94daf8fd157c32e571c85cb12e66692f15516a83a03264b08d06c3"}, + {file = "charset_normalizer-3.4.4-cp38-cp38-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:c4ef880e27901b6cc782f1b95f82da9313c0eb95c3af699103088fa0ac3ce9ac"}, + {file = "charset_normalizer-3.4.4-cp38-cp38-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:2aaba3b0819274cc41757a1da876f810a3e4d7b6eb25699253a4effef9e8e4af"}, + {file = "charset_normalizer-3.4.4-cp38-cp38-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:778d2e08eda00f4256d7f672ca9fef386071c9202f5e4607920b86d7803387f2"}, + {file = "charset_normalizer-3.4.4-cp38-cp38-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f155a433c2ec037d4e8df17d18922c3a0d9b3232a396690f17175d2946f0218d"}, + {file = "charset_normalizer-3.4.4-cp38-cp38-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a8bf8d0f749c5757af2142fe7903a9df1d2e8aa3841559b2bad34b08d0e2bcf3"}, + {file = "charset_normalizer-3.4.4-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:194f08cbb32dc406d6e1aea671a68be0823673db2832b38405deba2fb0d88f63"}, + {file = "charset_normalizer-3.4.4-cp38-cp38-musllinux_1_2_armv7l.whl", hash = "sha256:6aee717dcfead04c6eb1ce3bd29ac1e22663cdea57f943c87d1eab9a025438d7"}, + {file = "charset_normalizer-3.4.4-cp38-cp38-musllinux_1_2_ppc64le.whl", hash = "sha256:cd4b7ca9984e5e7985c12bc60a6f173f3c958eae74f3ef6624bb6b26e2abbae4"}, + {file = "charset_normalizer-3.4.4-cp38-cp38-musllinux_1_2_riscv64.whl", hash = "sha256:b7cf1017d601aa35e6bb650b6ad28652c9cd78ee6caff19f3c28d03e1c80acbf"}, + {file = "charset_normalizer-3.4.4-cp38-cp38-musllinux_1_2_s390x.whl", hash = "sha256:e912091979546adf63357d7e2ccff9b44f026c075aeaf25a52d0e95ad2281074"}, + {file = "charset_normalizer-3.4.4-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:5cb4d72eea50c8868f5288b7f7f33ed276118325c1dfd3957089f6b519e1382a"}, + {file = "charset_normalizer-3.4.4-cp38-cp38-win32.whl", hash = "sha256:837c2ce8c5a65a2035be9b3569c684358dfbf109fd3b6969630a87535495ceaa"}, + {file = "charset_normalizer-3.4.4-cp38-cp38-win_amd64.whl", hash = "sha256:44c2a8734b333e0578090c4cd6b16f275e07aa6614ca8715e6c038e865e70576"}, + {file = "charset_normalizer-3.4.4-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:a9768c477b9d7bd54bc0c86dbaebdec6f03306675526c9927c0e8a04e8f94af9"}, + {file = "charset_normalizer-3.4.4-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1bee1e43c28aa63cb16e5c14e582580546b08e535299b8b6158a7c9c768a1f3d"}, + {file = "charset_normalizer-3.4.4-cp39-cp39-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:fd44c878ea55ba351104cb93cc85e74916eb8fa440ca7903e57575e97394f608"}, + {file = "charset_normalizer-3.4.4-cp39-cp39-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:0f04b14ffe5fdc8c4933862d8306109a2c51e0704acfa35d51598eb45a1e89fc"}, + {file = "charset_normalizer-3.4.4-cp39-cp39-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:cd09d08005f958f370f539f186d10aec3377d55b9eeb0d796025d4886119d76e"}, + {file = "charset_normalizer-3.4.4-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4fe7859a4e3e8457458e2ff592f15ccb02f3da787fcd31e0183879c3ad4692a1"}, + {file = "charset_normalizer-3.4.4-cp39-cp39-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:fa09f53c465e532f4d3db095e0c55b615f010ad81803d383195b6b5ca6cbf5f3"}, + {file = "charset_normalizer-3.4.4-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:7fa17817dc5625de8a027cb8b26d9fefa3ea28c8253929b8d6649e705d2835b6"}, + {file = "charset_normalizer-3.4.4-cp39-cp39-musllinux_1_2_armv7l.whl", hash = "sha256:5947809c8a2417be3267efc979c47d76a079758166f7d43ef5ae8e9f92751f88"}, + {file = "charset_normalizer-3.4.4-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:4902828217069c3c5c71094537a8e623f5d097858ac6ca8252f7b4d10b7560f1"}, + {file = "charset_normalizer-3.4.4-cp39-cp39-musllinux_1_2_riscv64.whl", hash = "sha256:7c308f7e26e4363d79df40ca5b2be1c6ba9f02bdbccfed5abddb7859a6ce72cf"}, + {file = "charset_normalizer-3.4.4-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:2c9d3c380143a1fedbff95a312aa798578371eb29da42106a29019368a475318"}, + {file = "charset_normalizer-3.4.4-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:cb01158d8b88ee68f15949894ccc6712278243d95f344770fa7593fa2d94410c"}, + {file = "charset_normalizer-3.4.4-cp39-cp39-win32.whl", hash = "sha256:2677acec1a2f8ef614c6888b5b4ae4060cc184174a938ed4e8ef690e15d3e505"}, + {file = "charset_normalizer-3.4.4-cp39-cp39-win_amd64.whl", hash = "sha256:f8e160feb2aed042cd657a72acc0b481212ed28b1b9a95c0cee1621b524e1966"}, + {file = "charset_normalizer-3.4.4-cp39-cp39-win_arm64.whl", hash = "sha256:b5d84d37db046c5ca74ee7bb47dd6cbc13f80665fdde3e8040bdd3fb015ecb50"}, + {file = "charset_normalizer-3.4.4-py3-none-any.whl", hash = "sha256:7a32c560861a02ff789ad905a2fe94e3f840803362c84fecf1851cb4cf3dc37f"}, + {file = "charset_normalizer-3.4.4.tar.gz", hash = "sha256:94537985111c35f28720e43603b8e7b43a6ecfb2ce1d3058bbe955b73404e21a"}, ] [[package]] @@ -386,14 +441,14 @@ files = [ [[package]] name = "constantly" -version = "15.1.0" +version = "23.10.4" description = "Symbolic constants in Python" optional = false -python-versions = "*" +python-versions = ">=3.8" groups = ["main"] files = [ - {file = "constantly-15.1.0-py2.py3-none-any.whl", hash = "sha256:dd2fa9d6b1a51a83f0d7dd76293d734046aa176e384bf6e33b7e44880eb37c5d"}, - {file = "constantly-15.1.0.tar.gz", hash = "sha256:586372eb92059873e29eba4f9dec8381541b4d3834660707faf8ba59146dfc35"}, + {file = "constantly-23.10.4-py3-none-any.whl", hash = "sha256:3fd9b4d1c3dc1ec9757f3c52aef7e53ad9323dbe39f51dfd4c43853b68dfa3f9"}, + {file = "constantly-23.10.4.tar.gz", hash = "sha256:aa92b70a33e2ac0bb33cd745eb61776594dc48764b06c35e0efd050b7f1c7cbd"}, ] [[package]] @@ -476,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 == \"all\" or extra == \"saml2\"" +markers = "extra == \"saml2\" or extra == \"all\"" files = [ {file = "defusedxml-0.7.1-py2.py3-none-any.whl", hash = "sha256:a352e7e428770286cc899e2542b6cdaedb2b4953ff269a210103ec58f6198a61"}, {file = "defusedxml-0.7.1.tar.gz", hash = "sha256:1bb3032db185915b62d7c6209c5a8792be6a32ab2fedacc84e01b52c51aa3e69"}, @@ -484,42 +539,42 @@ files = [ [[package]] name = "docutils" -version = "0.19" +version = "0.22.4" description = "Docutils -- Python Documentation Utilities" optional = false -python-versions = ">=3.7" +python-versions = ">=3.9" groups = ["dev"] files = [ - {file = "docutils-0.19-py3-none-any.whl", hash = "sha256:5e1de4d849fee02c63b040a4a3fd567f4ab104defd8a5511fbbc24a8a017efbc"}, - {file = "docutils-0.19.tar.gz", hash = "sha256:33995a6753c30b7f577febfc2c50411fec6aac7f7ffeb7c4cfe5991072dcf9e6"}, + {file = "docutils-0.22.4-py3-none-any.whl", hash = "sha256:d0013f540772d1420576855455d050a2180186c91c15779301ac2ccb3eeb68de"}, + {file = "docutils-0.22.4.tar.gz", hash = "sha256:4db53b1fde9abecbb74d91230d32ab626d94f6badfc575d6db9194a49df29968"}, ] [[package]] name = "elementpath" -version = "4.1.5" +version = "4.8.0" description = "XPath 1.0/2.0/3.0/3.1 parsers and selectors for ElementTree and lxml" optional = true -python-versions = ">=3.7" +python-versions = ">=3.8" groups = ["main"] -markers = "extra == \"all\" or extra == \"saml2\"" +markers = "extra == \"saml2\" or extra == \"all\"" files = [ - {file = "elementpath-4.1.5-py3-none-any.whl", hash = "sha256:2ac1a2fb31eb22bbbf817f8cf6752f844513216263f0e3892c8e79782fe4bb55"}, - {file = "elementpath-4.1.5.tar.gz", hash = "sha256:c2d6dc524b29ef751ecfc416b0627668119d8812441c555d7471da41d4bacb8d"}, + {file = "elementpath-4.8.0-py3-none-any.whl", hash = "sha256:5393191f84969bcf8033b05ec4593ef940e58622ea13cefe60ecefbbf09d58d9"}, + {file = "elementpath-4.8.0.tar.gz", hash = "sha256:5822a2560d99e2633d95f78694c7ff9646adaa187db520da200a8e9479dc46ae"}, ] [package.extras] -dev = ["Sphinx", "coverage", "flake8", "lxml", "lxml-stubs", "memory-profiler", "memray", "mypy", "tox", "xmlschema (>=2.0.0)"] +dev = ["Sphinx", "coverage", "flake8", "lxml", "lxml-stubs", "memory-profiler", "memray", "mypy", "tox", "xmlschema (>=3.3.2)"] [[package]] name = "gitdb" -version = "4.0.10" +version = "4.0.12" description = "Git Object Database" optional = false python-versions = ">=3.7" groups = ["dev"] files = [ - {file = "gitdb-4.0.10-py3-none-any.whl", hash = "sha256:c286cf298426064079ed96a9e4a9d39e7f3e9bf15ba60701e95f5492f28415c7"}, - {file = "gitdb-4.0.10.tar.gz", hash = "sha256:6eb990b69df4e15bad899ea868dc46572c3f75339735663b81de79b06f17eb9a"}, + {file = "gitdb-4.0.12-py3-none-any.whl", hash = "sha256:67073e15955400952c6565cc3e707c554a4eea2e428946f7a4c162fab9bd9bcf"}, + {file = "gitdb-4.0.12.tar.gz", hash = "sha256:5ef71f855d191a3326fcfbc0d5da835f26b13fbcba60c32c21091c349ffdb571"}, ] [package.dependencies] @@ -527,14 +582,14 @@ smmap = ">=3.0.1,<6" [[package]] name = "gitpython" -version = "3.1.45" +version = "3.1.46" description = "GitPython is a Python library used to interact with Git repositories" optional = false python-versions = ">=3.7" groups = ["dev"] files = [ - {file = "gitpython-3.1.45-py3-none-any.whl", hash = "sha256:8908cb2e02fb3b93b7eb0f2827125cb699869470432cc885f019b8fd0fccff77"}, - {file = "gitpython-3.1.45.tar.gz", hash = "sha256:85b0ee964ceddf211c41b9f27a49086010a190fd8132a24e21f362a4b36a791c"}, + {file = "gitpython-3.1.46-py3-none-any.whl", hash = "sha256:79812ed143d9d25b6d176a10bb511de0f9c67b1fa641d82097b0ab90398a2058"}, + {file = "gitpython-3.1.46.tar.gz", hash = "sha256:400124c7d0ef4ea03f7310ac2fbf7151e09ff97f2a3288d64a440c584a29c37f"}, ] [package.dependencies] @@ -542,7 +597,7 @@ gitdb = ">=4.0.1,<5" [package.extras] doc = ["sphinx (>=7.1.2,<7.2)", "sphinx-autodoc-typehints", "sphinx_rtd_theme"] -test = ["coverage[toml]", "ddt (>=1.1.1,!=1.4.3)", "mock ; python_version < \"3.8\"", "mypy", "pre-commit", "pytest (>=7.3.1)", "pytest-cov", "pytest-instafail", "pytest-mock", "pytest-sugar", "typing-extensions ; python_version < \"3.11\""] +test = ["coverage[toml]", "ddt (>=1.1.1,!=1.4.3)", "mock ; python_version < \"3.8\"", "mypy (==1.18.2) ; python_version >= \"3.9\"", "pre-commit", "pytest (>=7.3.1)", "pytest-cov", "pytest-instafail", "pytest-mock", "pytest-sugar", "typing-extensions ; python_version < \"3.11\""] [[package]] name = "hiredis" @@ -551,7 +606,7 @@ description = "Python wrapper for hiredis" optional = true python-versions = ">=3.8" groups = ["main"] -markers = "extra == \"all\" or extra == \"redis\"" +markers = "extra == \"redis\" or extra == \"all\"" 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"}, @@ -677,22 +732,22 @@ idna = ">=2.5" [[package]] name = "id" -version = "1.5.0" +version = "1.6.1" description = "A tool for generating OIDC identities" optional = false -python-versions = ">=3.8" +python-versions = ">=3.9" groups = ["dev"] files = [ - {file = "id-1.5.0-py3-none-any.whl", hash = "sha256:f1434e1cef91f2cbb8a4ec64663d5a23b9ed43ef44c4c957d02583d61714c658"}, - {file = "id-1.5.0.tar.gz", hash = "sha256:292cb8a49eacbbdbce97244f47a97b4c62540169c976552e497fd57df0734c1d"}, + {file = "id-1.6.1-py3-none-any.whl", hash = "sha256:f5ec41ed2629a508f5d0988eda142e190c9c6da971100612c4de9ad9f9b237ca"}, + {file = "id-1.6.1.tar.gz", hash = "sha256:d0732d624fb46fd4e7bc4e5152f00214450953b9e772c182c1c22964def1a069"}, ] [package.dependencies] -requests = "*" +urllib3 = ">=2,<3" [package.extras] dev = ["build", "bump (>=1.3.2)", "id[lint,test]"] -lint = ["bandit", "interrogate", "mypy", "ruff (<0.8.2)", "types-requests"] +lint = ["bandit", "interrogate", "mypy", "ruff (<0.14.15)"] test = ["coverage[toml]", "pretend", "pytest", "pytest-cov"] [[package]] @@ -712,161 +767,162 @@ all = ["flake8 (>=7.1.1)", "mypy (>=1.11.2)", "pytest (>=8.3.2)", "ruff (>=0.6.2 [[package]] name = "ijson" -version = "3.4.0.post0" +version = "3.5.0" description = "Iterative JSON parser with standard Python iterator interfaces" optional = false python-versions = ">=3.9" groups = ["main"] files = [ - {file = "ijson-3.4.0.post0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:8f904a405b58a04b6ef0425f1babbc5c65feb66b0a4cc7f214d4ad7de106f77d"}, - {file = "ijson-3.4.0.post0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:a07dcc1a8a1ddd76131a7c7528cbd12951c2e34eb3c3d63697b905069a2d65b1"}, - {file = "ijson-3.4.0.post0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:ab3be841b8c430c1883b8c0775eb551f21b5500c102c7ee828afa35ddd701bdd"}, - {file = "ijson-3.4.0.post0-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:43059ae0d657b11c5ddb11d149bc400c44f9e514fb8663057e9b2ea4d8d44c1f"}, - {file = "ijson-3.4.0.post0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0d3e82963096579d1385c06b2559570d7191e225664b7fa049617da838e1a4a4"}, - {file = "ijson-3.4.0.post0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:461ce4e87a21a261b60c0a68a2ad17c7dd214f0b90a0bec7e559a66b6ae3bd7e"}, - {file = "ijson-3.4.0.post0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:890cf6610c9554efcb9765a93e368efeb5bb6135f59ce0828d92eaefff07fde5"}, - {file = "ijson-3.4.0.post0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:6793c29a5728e7751a7df01be58ba7da9b9690c12bf79d32094c70a908fa02b9"}, - {file = "ijson-3.4.0.post0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:a56b6674d7feec0401c91f86c376f4e3d8ff8129128a8ad21ca43ec0b1242f79"}, - {file = "ijson-3.4.0.post0-cp310-cp310-win32.whl", hash = "sha256:01767fcbd75a5fa5a626069787b41f04681216b798510d5f63bcf66884386368"}, - {file = "ijson-3.4.0.post0-cp310-cp310-win_amd64.whl", hash = "sha256:09127c06e5dec753feb9e4b8c5f6a23603d1cd672d098159a17e53a73b898eec"}, - {file = "ijson-3.4.0.post0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:0b473112e72c0c506da425da3278367b6680f340ecc093084693a1e819d28435"}, - {file = "ijson-3.4.0.post0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:043f9b7cf9cc744263a78175e769947733710d2412d25180df44b1086b23ebd5"}, - {file = "ijson-3.4.0.post0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:b55e49045f4c8031f3673f56662fd828dc9e8d65bd3b03a9420dda0d370e64ba"}, - {file = "ijson-3.4.0.post0-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:11f13b73194ea2a5a8b4a2863f25b0b4624311f10db3a75747b510c4958179b0"}, - {file = "ijson-3.4.0.post0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:659acb2843433e080c271ecedf7d19c71adde1ee5274fc7faa2fec0a793f9f1c"}, - {file = "ijson-3.4.0.post0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:deda4cfcaafa72ca3fa845350045b1d0fef9364ec9f413241bb46988afbe6ee6"}, - {file = "ijson-3.4.0.post0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:47352563e8c594360bacee2e0753e97025f0861234722d02faace62b1b6d2b2a"}, - {file = "ijson-3.4.0.post0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:5a48b9486242d1295abe7fd0fbb6308867da5ca3f69b55c77922a93c2b6847aa"}, - {file = "ijson-3.4.0.post0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:9c0886234d1fae15cf4581a430bdba03d79251c1ab3b07e30aa31b13ef28d01c"}, - {file = "ijson-3.4.0.post0-cp311-cp311-win32.whl", hash = "sha256:fecae19b5187d92900c73debb3a979b0b3290a53f85df1f8f3c5ba7d1e9fb9cb"}, - {file = "ijson-3.4.0.post0-cp311-cp311-win_amd64.whl", hash = "sha256:b39dbf87071f23a23c8077eea2ae7cfeeca9ff9ffec722dfc8b5f352e4dd729c"}, - {file = "ijson-3.4.0.post0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:b607a500fca26101be47d2baf7cddb457b819ab60a75ce51ed1092a40da8b2f9"}, - {file = "ijson-3.4.0.post0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:4827d9874a6a81625412c59f7ca979a84d01f7f6bfb3c6d4dc4c46d0382b14e0"}, - {file = "ijson-3.4.0.post0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:d4d4afec780881edb2a0d2dd40b1cdbe246e630022d5192f266172a0307986a7"}, - {file = "ijson-3.4.0.post0-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:432fb60ffb952926f9438e0539011e2dfcd108f8426ee826ccc6173308c3ff2c"}, - {file = "ijson-3.4.0.post0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:54a0e3e05d9a0c95ecba73d9579f146cf6d5c5874116c849dba2d39a5f30380e"}, - {file = "ijson-3.4.0.post0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:05807edc0bcbd222dc6ea32a2b897f0c81dc7f12c8580148bc82f6d7f5e7ec7b"}, - {file = "ijson-3.4.0.post0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:a5269af16f715855d9864937f9dd5c348ca1ac49cee6a2c7a1b7091c159e874f"}, - {file = "ijson-3.4.0.post0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:b200df83c901f5bfa416d069ac71077aa1608f854a4c50df1b84ced560e9c9ec"}, - {file = "ijson-3.4.0.post0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:6458bd8e679cdff459a0a5e555b107c3bbacb1f382da3fe0f40e392871eb518d"}, - {file = "ijson-3.4.0.post0-cp312-cp312-win32.whl", hash = "sha256:55f7f656b5986326c978cbb3a9eea9e33f3ef6ecc4535b38f1d452c731da39ab"}, - {file = "ijson-3.4.0.post0-cp312-cp312-win_amd64.whl", hash = "sha256:e15833dcf6f6d188fdc624a31cd0520c3ba21b6855dc304bc7c1a8aeca02d4ac"}, - {file = "ijson-3.4.0.post0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:114ed248166ac06377e87a245a158d6b98019d2bdd3bb93995718e0bd996154f"}, - {file = "ijson-3.4.0.post0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:ffb21203736b08fe27cb30df6a4f802fafb9ef7646c5ff7ef79569b63ea76c57"}, - {file = "ijson-3.4.0.post0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:07f20ecd748602ac7f18c617637e53bd73ded7f3b22260bba3abe401a7fc284e"}, - {file = "ijson-3.4.0.post0-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:27aa193d47ffc6bc4e45453896ad98fb089a367e8283b973f1fe5c0198b60b4e"}, - {file = "ijson-3.4.0.post0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ccddb2894eb7af162ba43b9475ac5825d15d568832f82eb8783036e5d2aebd42"}, - {file = "ijson-3.4.0.post0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:61ab0b8c5bf707201dc67e02c116f4b6545c4afd7feb2264b989d242d9c4348a"}, - {file = "ijson-3.4.0.post0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:254cfb8c124af68327a0e7a49b50bbdacafd87c4690a3d62c96eb01020a685ef"}, - {file = "ijson-3.4.0.post0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:04ac9ca54db20f82aeda6379b5f4f6112fdb150d09ebce04affeab98a17b4ed3"}, - {file = "ijson-3.4.0.post0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a603d7474bf35e7b3a8e49c8dabfc4751841931301adff3f3318171c4e407f32"}, - {file = "ijson-3.4.0.post0-cp313-cp313-win32.whl", hash = "sha256:ec5bb1520cb212ebead7dba048bb9b70552c3440584f83b01b0abc96862e2a09"}, - {file = "ijson-3.4.0.post0-cp313-cp313-win_amd64.whl", hash = "sha256:3505dff18bdeb8b171eb28af6df34857e2be80dc01e2e3b624e77215ad58897f"}, - {file = "ijson-3.4.0.post0-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:45a0b1c833ed2620eaf8da958f06ac8351c59e5e470e078400d23814670ed708"}, - {file = "ijson-3.4.0.post0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:7809ec8c8f40228edaaa089f33e811dff4c5b8509702652870d3f286c9682e27"}, - {file = "ijson-3.4.0.post0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:cf4a34c2cfe852aee75c89c05b0a4531c49dc0be27eeed221afd6fbf9c3e149c"}, - {file = "ijson-3.4.0.post0-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:a39d5d36067604b26b78de70b8951c90e9272450642661fe531a8f7a6936a7fa"}, - {file = "ijson-3.4.0.post0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:83fc738d81c9ea686b452996110b8a6678296c481e0546857db24785bff8da92"}, - {file = "ijson-3.4.0.post0-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b2a81aee91633868f5b40280e2523f7c5392e920a5082f47c5e991e516b483f6"}, - {file = "ijson-3.4.0.post0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:56169e298c5a2e7196aaa55da78ddc2415876a74fe6304f81b1eb0d3273346f7"}, - {file = "ijson-3.4.0.post0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:eeb9540f0b1a575cbb5968166706946458f98c16e7accc6f2fe71efa29864241"}, - {file = "ijson-3.4.0.post0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:ba3478ff0bb49d7ba88783f491a99b6e3fa929c930ab062d2bb7837e6a38fe88"}, - {file = "ijson-3.4.0.post0-cp313-cp313t-win32.whl", hash = "sha256:b005ce84e82f28b00bf777a464833465dfe3efa43a0a26c77b5ac40723e1a728"}, - {file = "ijson-3.4.0.post0-cp313-cp313t-win_amd64.whl", hash = "sha256:fe9c84c9b1c8798afa407be1cea1603401d99bfc7c34497e19f4f5e5ddc9b441"}, - {file = "ijson-3.4.0.post0-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:da6a21b88cbf5ecbc53371283988d22c9643aa71ae2873bbeaefd2dea3b6160b"}, - {file = "ijson-3.4.0.post0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:cf24a48a1c3ca9d44a04feb59ccefeb9aa52bb49b9cb70ad30518c25cce74bb7"}, - {file = "ijson-3.4.0.post0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:d14427d366f95f21adcb97d0ed1f6d30f6fdc04d0aa1e4de839152c50c2b8d65"}, - {file = "ijson-3.4.0.post0-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:339d49f6c5d24051c85d9226be96d2d56e633cb8b7d09dd8099de8d8b51a97e2"}, - {file = "ijson-3.4.0.post0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7206afcb396aaef66c2b066997b4e9d9042c4b7d777f4d994e9cec6d322c2fe6"}, - {file = "ijson-3.4.0.post0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c8dd327da225887194fe8b93f2b3c9c256353e14a6b9eefc940ed17fde38f5b8"}, - {file = "ijson-3.4.0.post0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:4810546e66128af51fd4a0c9a640e84e8508e9c15c4f247d8a3e3253b20e1465"}, - {file = "ijson-3.4.0.post0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:103a0838061297d063bca81d724b0958b616f372bd893bbc278320152252c652"}, - {file = "ijson-3.4.0.post0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:40007c977e230e04118b27322f25a72ae342a3d61464b2057fcd9b21eeb7427a"}, - {file = "ijson-3.4.0.post0-cp314-cp314-win32.whl", hash = "sha256:f932969fc1fd4449ca141cf5f47ff357656a154a361f28d9ebca0badc5b02297"}, - {file = "ijson-3.4.0.post0-cp314-cp314-win_amd64.whl", hash = "sha256:3ed19b1e4349240773a8ce4a4bfa450892d4a57949c02c515cd6be5a46b7696a"}, - {file = "ijson-3.4.0.post0-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:226447e40ca9340a39ed07d68ea02ee14b52cb4fe649425b256c1f0073531c83"}, - {file = "ijson-3.4.0.post0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:2c88f0669d45d4b1aa017c9b68d378e7cd15d188dfb6f0209adc78b7f45590a7"}, - {file = "ijson-3.4.0.post0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:56b3089dc28c12492d92cc4896d2be585a89ecae34e25d08c1df88f21815cb50"}, - {file = "ijson-3.4.0.post0-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:c117321cfa7b749cc1213f9b4c80dc958f0a206df98ec038ae4bcbbdb8463a15"}, - {file = "ijson-3.4.0.post0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8311f48db6a33116db5c81682f08b6e2405501a4b4e460193ae69fec3cd1f87a"}, - {file = "ijson-3.4.0.post0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:91c61a3e63e04da648737e6b4abd537df1b46fb8cdf3219b072e790bb3c1a46b"}, - {file = "ijson-3.4.0.post0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:1709171023ce82651b2f132575c2e6282e47f64ad67bd3260da476418d0e7895"}, - {file = "ijson-3.4.0.post0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:5f0a72b1e3c0f78551670c12b2fdc1bf05f2796254d9c2055ba319bec2216020"}, - {file = "ijson-3.4.0.post0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:b982a3597b0439ce9c8f4cfc929d86c6ed43907908be1e8463a34dc35fe5b258"}, - {file = "ijson-3.4.0.post0-cp314-cp314t-win32.whl", hash = "sha256:4e39bfdc36b0b460ef15a06550a6a385c64c81f7ac205ccff39bd45147918912"}, - {file = "ijson-3.4.0.post0-cp314-cp314t-win_amd64.whl", hash = "sha256:17e45262a5ddef39894013fb1548ee7094e444c8389eb1a97f86708b19bea03e"}, - {file = "ijson-3.4.0.post0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:35eb2760a42fd9461358b4be131287587b49ff504fc37fa3014dca6c27c343f4"}, - {file = "ijson-3.4.0.post0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:f82ca7abfb3ef3cf2194c71dad634572bcccd62a5dd466649f78fe73d492c860"}, - {file = "ijson-3.4.0.post0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:97f5ef3d839fc24b0ad47e8b31b4751ae72c5d83606e3ee4c92bb25965c03a4f"}, - {file = "ijson-3.4.0.post0-cp39-cp39-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:a2c873742e9f7e21378516217d81d6fa11d34bae860ed364832c00ab1dbf37ed"}, - {file = "ijson-3.4.0.post0-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2f8b9ffa2c2dfe3289da9aec4e5ab52684fa2b2da2c853c7891b360ec46fba07"}, - {file = "ijson-3.4.0.post0-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0634b21188c67e5cf471cc1d30d193d19f521d89e2125ab1fb602aa8ae61e050"}, - {file = "ijson-3.4.0.post0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:3752dd6f51ef58a71799de745649deff293e959700f1b7f5b1989618da366f24"}, - {file = "ijson-3.4.0.post0-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:57db77f4ea3eca09f519f627d9f9c76eb862b30edef5d899f031feeed94f05a1"}, - {file = "ijson-3.4.0.post0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:435270a4b75667305f6df3226e5224e83cd6906022d7fdcc9df05caae725f796"}, - {file = "ijson-3.4.0.post0-cp39-cp39-win32.whl", hash = "sha256:742c211b004ab51ccad2b301525d8a6eb2cf68a5fb82d78836f3a351eec44d4e"}, - {file = "ijson-3.4.0.post0-cp39-cp39-win_amd64.whl", hash = "sha256:35aaa979da875fa92bea5dc5969b1541b4912b165091761785459a43f0c20946"}, - {file = "ijson-3.4.0.post0-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:add9242f886eae844a7410b84aee2bbb8bdc83c624f227cb1fdb2d0476a96cb1"}, - {file = "ijson-3.4.0.post0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:69718ed41710dfcaa7564b0af42abc05875d4f7aaa24627c808867ef32634bc7"}, - {file = "ijson-3.4.0.post0-pp311-pypy311_pp73-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:636b6eca96c6c43c04629c6b37fad0181662eaacf9877c71c698485637f752f9"}, - {file = "ijson-3.4.0.post0-pp311-pypy311_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:eb5e73028f6e63d27b3d286069fe350ed80a4ccc493b022b590fea4bb086710d"}, - {file = "ijson-3.4.0.post0-pp311-pypy311_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:461acf4320219459dabe5ed90a45cb86c9ba8cc6d6db9dad0d9427d42f57794c"}, - {file = "ijson-3.4.0.post0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:a0fedf09c0f6ffa2a99e7e7fd9c5f3caf74e655c1ee015a0797383e99382ebc3"}, - {file = "ijson-3.4.0.post0.tar.gz", hash = "sha256:9aa02dc70bb245670a6ca7fba737b992aeeb4895360980622f7e568dbf23e41e"}, + {file = "ijson-3.5.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:ea8dcac10d86adaeead454bc25c97b68d0bda573d5fd6f86f5e21cf8f7906f88"}, + {file = "ijson-3.5.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:92b0495bbb2150bbf14fc5d98fb6d76bcd1c526605a172709e602e6fedc96495"}, + {file = "ijson-3.5.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:7af0c4c8943be8b09a4e57bdc1da6001dae7b36526d4154fe5c8224738d0921f"}, + {file = "ijson-3.5.0-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:45887d5e84ff0d2b138c926cebd9071830733968afe8d9d12080b3c178c7f918"}, + {file = "ijson-3.5.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9a70b575be8e57a28c80e90ed349ad3a851c3478524c70e36e07d6092ecd12c9"}, + {file = "ijson-3.5.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2adeecd45830bfd5580ca79a584154713aabef0b9607e16249133df5d2859813"}, + {file = "ijson-3.5.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:d873e72889e7fc5962ab58909f1adff338d7c2f49e450e5b5fe844eff8155a14"}, + {file = "ijson-3.5.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:9a88c559456a79708592234d697645d92b599718f4cbbeaa6515f83ac63ca0ae"}, + {file = "ijson-3.5.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:cf83f58ad50dc0d39a2105cb26d4f359b38f42cef68b913170d4d47d97d97ba5"}, + {file = "ijson-3.5.0-cp310-cp310-win32.whl", hash = "sha256:aec4580a7712a19b1f95cd41bed260fc6a31266d37ef941827772a4c199e8143"}, + {file = "ijson-3.5.0-cp310-cp310-win_amd64.whl", hash = "sha256:9a9c4c70501e23e8eb1675330686d1598eebfa14b6f0dbc8f00c2e081cc628fa"}, + {file = "ijson-3.5.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:5616311404b858d32740b7ad8b9a799c62165f5ecb85d0a8ed16c21665a90533"}, + {file = "ijson-3.5.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:e9733f94029dd41702d573ef64752e2556e72aea14623d6dbb7a44ca1ccf30fd"}, + {file = "ijson-3.5.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:db8398c6721b98412a4f618da8022550c8b9c5d9214040646071b5deb4d4a393"}, + {file = "ijson-3.5.0-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:c061314845c08163b1784b6076ea5f075372461a32e6916f4e5f211fd4130b64"}, + {file = "ijson-3.5.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1111a1c5ac79119c5d6e836f900c1a53844b50a18af38311baa6bb61e2645aca"}, + {file = "ijson-3.5.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1e74aff8c681c24002b61b1822f9511d4c384f324f7dbc08c78538e01fdc9fcb"}, + {file = "ijson-3.5.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:739a7229b1b0cc5f7e2785a6e7a5fc915e850d3fed9588d0e89a09f88a417253"}, + {file = "ijson-3.5.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:ef88712160360cab3ca6471a4e5418243f8b267cf1fe1620879d1b5558babc71"}, + {file = "ijson-3.5.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:6ca0d1b6b5f8166a6248f4309497585fb8553b04bc8179a0260fad636cfdb798"}, + {file = "ijson-3.5.0-cp311-cp311-win32.whl", hash = "sha256:966039cf9047c7967febf7b9a52ec6f38f5464a4c7fbb5565e0224b7376fefff"}, + {file = "ijson-3.5.0-cp311-cp311-win_amd64.whl", hash = "sha256:6bad6a1634cb7c9f3f4c7e52325283b35b565f5b6cc27d42660c6912ce883422"}, + {file = "ijson-3.5.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:1ebefbe149a6106cc848a3eaf536af51a9b5ccc9082de801389f152dba6ab755"}, + {file = "ijson-3.5.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:19e30d9f00f82e64de689c0b8651b9cfed879c184b139d7e1ea5030cec401c21"}, + {file = "ijson-3.5.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:a04a33ee78a6f27b9b8528c1ca3c207b1df3b8b867a4cf2fcc4109986f35c227"}, + {file = "ijson-3.5.0-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:7d48dc2984af02eb3c56edfb3f13b3f62f2f3e4fe36f058c8cfc75d93adf4fed"}, + {file = "ijson-3.5.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f1e73a44844d9adbca9cf2c4132cd875933e83f3d4b23881fcaf82be83644c7d"}, + {file = "ijson-3.5.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7389a56b8562a19948bdf1d7bae3a2edc8c7f86fb59834dcb1c4c722818e645a"}, + {file = "ijson-3.5.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3176f23f8ebec83f374ed0c3b4e5a0c4db7ede54c005864efebbed46da123608"}, + {file = "ijson-3.5.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:6babd88e508630c6ef86c9bebaaf13bb2fb8ec1d8f8868773a03c20253f599bc"}, + {file = "ijson-3.5.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:dc1b3836b174b6db2fa8319f1926fb5445abd195dc963368092103f8579cb8ed"}, + {file = "ijson-3.5.0-cp312-cp312-win32.whl", hash = "sha256:6673de9395fb9893c1c79a43becd8c8fbee0a250be6ea324bfd1487bb5e9ee4c"}, + {file = "ijson-3.5.0-cp312-cp312-win_amd64.whl", hash = "sha256:f4f7fabd653459dcb004175235f310435959b1bb5dfa8878578391c6cc9ad944"}, + {file = "ijson-3.5.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:e9cedc10e40dd6023c351ed8bfc7dcfce58204f15c321c3c1546b9c7b12562a4"}, + {file = "ijson-3.5.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:3647649f782ee06c97490b43680371186651f3f69bebe64c6083ee7615d185e5"}, + {file = "ijson-3.5.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:90e74be1dce05fce73451c62d1118671f78f47c9f6be3991c82b91063bf01fc9"}, + {file = "ijson-3.5.0-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:78e9ad73e7be2dd80627504bd5cbf512348c55ce2c06e362ed7683b5220e8568"}, + {file = "ijson-3.5.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9577449313cc94be89a4fe4b3e716c65f09cc19636d5a6b2861c4e80dddebd58"}, + {file = "ijson-3.5.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3e4c1178fb50aff5f5701a30a5152ead82a14e189ce0f6102fa1b5f10b2f54ff"}, + {file = "ijson-3.5.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:0eb402ab026ffb37a918d75af2b7260fe6cfbce13232cc83728a714dd30bd81d"}, + {file = "ijson-3.5.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:5b08ee08355f9f729612a8eb9bf69cc14f9310c3b2a487c6f1c3c65d85216ec4"}, + {file = "ijson-3.5.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:bda62b6d48442903e7bf56152108afb7f0f1293c2b9bef2f2c369defea76ab18"}, + {file = "ijson-3.5.0-cp313-cp313-win32.whl", hash = "sha256:8d073d9b13574cfa11083cc7267c238b7a6ed563c2661e79192da4a25f09c82c"}, + {file = "ijson-3.5.0-cp313-cp313-win_amd64.whl", hash = "sha256:2419f9e32e0968a876b04d8f26aeac042abd16f582810b576936bbc4c6015069"}, + {file = "ijson-3.5.0-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:4d4b0cd676b8c842f7648c1a783448fac5cd3b98289abd83711b3e275e143524"}, + {file = "ijson-3.5.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:252dec3680a48bb82d475e36b4ae1b3a9d7eb690b951bb98a76c5fe519e30188"}, + {file = "ijson-3.5.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:aa1b5dca97d323931fde2501172337384c958914d81a9dac7f00f0d4bfc76bc7"}, + {file = "ijson-3.5.0-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:7a5ec7fd86d606094bba6f6f8f87494897102fa4584ef653f3005c51a784c320"}, + {file = "ijson-3.5.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:009f41443e1521847701c6d87fa3923c0b1961be3c7e7de90947c8cb92ea7c44"}, + {file = "ijson-3.5.0-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e4c3651d1f9fe2839a93fdf8fd1d5ca3a54975349894249f3b1b572bcc4bd577"}, + {file = "ijson-3.5.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:945b7abcfcfeae2cde17d8d900870f03536494245dda7ad4f8d056faa303256c"}, + {file = "ijson-3.5.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:0574b0a841ff97495c13e9d7260fbf3d85358b061f540c52a123db9dbbaa2ed6"}, + {file = "ijson-3.5.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:f969ffb2b89c5cdf686652d7fb66252bc72126fa54d416317411497276056a18"}, + {file = "ijson-3.5.0-cp313-cp313t-win32.whl", hash = "sha256:59d3f9f46deed1332ad669518b8099920512a78bda64c1f021fcd2aff2b36693"}, + {file = "ijson-3.5.0-cp313-cp313t-win_amd64.whl", hash = "sha256:5c2839fa233746d8aad3b8cd2354e441613f5df66d721d59da4a09394bd1db2b"}, + {file = "ijson-3.5.0-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:25a5a6b2045c90bb83061df27cfa43572afa43ba9408611d7bfe237c20a731a9"}, + {file = "ijson-3.5.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:8976c54c0b864bc82b951bae06567566ac77ef63b90a773a69cd73aab47f4f4f"}, + {file = "ijson-3.5.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:859eb2038f7f1b0664df4241957694cc35e6295992d71c98659b22c69b3cbc10"}, + {file = "ijson-3.5.0-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:c911aa02991c7c0d3639b6619b93a93210ff1e7f58bf7225d613abea10adc78e"}, + {file = "ijson-3.5.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:903cbdc350173605220edc19796fbea9b2203c8b3951fb7335abfa8ed37afda8"}, + {file = "ijson-3.5.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a4549d96ded5b8efa71639b2160235415f6bdb8c83367615e2dbabcb72755c33"}, + {file = "ijson-3.5.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:6b2dcf6349e6042d83f3f8c39ce84823cf7577eba25bac5aae5e39bbbbbe9c1c"}, + {file = "ijson-3.5.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:e44af39e6f8a17e5627dcd89715d8279bf3474153ff99aae031a936e5c5572e5"}, + {file = "ijson-3.5.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:9260332304b7e7828db56d43f08fc970a3ab741bf84ff10189361ea1b60c395b"}, + {file = "ijson-3.5.0-cp314-cp314-win32.whl", hash = "sha256:63bc8121bb422f6969ced270173a3fa692c29d4ae30c860a2309941abd81012a"}, + {file = "ijson-3.5.0-cp314-cp314-win_amd64.whl", hash = "sha256:01b6dad72b7b7df225ef970d334556dfad46c696a2c6767fb5d9ed8889728bca"}, + {file = "ijson-3.5.0-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:2ea4b676ec98e374c1df400a47929859e4fa1239274339024df4716e802aa7e4"}, + {file = "ijson-3.5.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:014586eec043e23c80be9a923c56c3a0920a0f1f7d17478ce7bc20ba443968ef"}, + {file = "ijson-3.5.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:d5b8b886b0248652d437f66e7c5ac318bbdcb2c7137a7e5327a68ca00b286f5f"}, + {file = "ijson-3.5.0-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:498fd46ae2349297e43acf97cdc421e711dbd7198418677259393d2acdc62d78"}, + {file = "ijson-3.5.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:22a51b4f9b81f12793731cf226266d1de2112c3c04ba4a04117ad4e466897e05"}, + {file = "ijson-3.5.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9636c710dc4ac4a281baa266a64f323b4cc165cec26836af702c44328b59a515"}, + {file = "ijson-3.5.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:f7168a39e8211107666d71b25693fd1b2bac0b33735ef744114c403c6cac21e1"}, + {file = "ijson-3.5.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:8696454245415bc617ab03b0dc3ae4c86987df5dc6a90bad378fe72c5409d89e"}, + {file = "ijson-3.5.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:c21bfb61f71f191565885bf1bc29e0a186292d866b4880637b833848360bdc1b"}, + {file = "ijson-3.5.0-cp314-cp314t-win32.whl", hash = "sha256:a2619460d6795b70d0155e5bf016200ac8a63ab5397aa33588bb02b6c21759e6"}, + {file = "ijson-3.5.0-cp314-cp314t-win_amd64.whl", hash = "sha256:4f24b78d4ef028d17eb57ad1b16c0aed4a17bdd9badbf232dc5d9305b7e13854"}, + {file = "ijson-3.5.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:0ec62d397447cbe4941818c53e22b054e03250ff9cdbaea75144b11bc6db44ed"}, + {file = "ijson-3.5.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:75980237a16e5e36ad46fbdd33e3f3d817c187624974c48947df0a2bfa104b9e"}, + {file = "ijson-3.5.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:a9c321e8e1cdeac8aac698d09a90d98a049c9be8e8330c89cf2fcc517c96d51d"}, + {file = "ijson-3.5.0-cp39-cp39-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:92878b130d7ad71919c70b4f50ad23ec7fbf2d09a9c675f9179d49c4be869a63"}, + {file = "ijson-3.5.0-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a1ab890d43656c1d12c4a8dafb7fac5a2278ed3e4408102e0971f48b6ed4583d"}, + {file = "ijson-3.5.0-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a55185e8983fef0b21abc1a0bbaa11eeb2fabdc651e2167f23defa9fe4eb999b"}, + {file = "ijson-3.5.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:5a3af031e30751164c3289294f249f942535fbe7e8f35eb3ecc374247449214e"}, + {file = "ijson-3.5.0-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:f4c8f5ccf7230a9a94c1d836322783ed0c0ec2a151f3d53b2e0a67c89ad66970"}, + {file = "ijson-3.5.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:6e249796d2090afc1c42d2458ab0dbf0072a30ffa246b5683e3f7b9dc9b1b7f9"}, + {file = "ijson-3.5.0-cp39-cp39-win32.whl", hash = "sha256:1b2cf2c0c79313fbc607a0d90788ffb4f4614872983af4aa85c5b92533ec4da2"}, + {file = "ijson-3.5.0-cp39-cp39-win_amd64.whl", hash = "sha256:d38cb03f6b7cc26d542ff710adfe98e5f6d53878461c45456c97d3668297ec0d"}, + {file = "ijson-3.5.0-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:d64c624da0e9d692d6eb0ff63a79656b59d76bf80773a17c5b0f835e4e8ef627"}, + {file = "ijson-3.5.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:876f7df73b7e0d6474f9caa729b9cdbfc8e76de9075a4887dfd689e29e85c4ca"}, + {file = "ijson-3.5.0-pp311-pypy311_pp73-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:e7dbff2c8d9027809b0cde663df44f3210da10ea377121d42896fb6ee405dd31"}, + {file = "ijson-3.5.0-pp311-pypy311_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4217a1edc278660679e1197c83a1a2a2d367792bfbb2a3279577f4b59b93730d"}, + {file = "ijson-3.5.0-pp311-pypy311_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:04f0fc740311388ee745ba55a12292b722d6f52000b11acbb913982ba5fbdf87"}, + {file = "ijson-3.5.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:fdeee6957f92e0c114f65c55cf8fe7eabb80cfacab64eea6864060913173f66d"}, + {file = "ijson-3.5.0.tar.gz", hash = "sha256:94688760720e3f5212731b3cb8d30267f9a045fb38fb3870254e7b9504246f31"}, ] [[package]] name = "immutabledict" -version = "4.2.2" +version = "4.3.1" description = "Immutable wrapper around dictionaries (a fork of frozendict)" optional = false python-versions = "<4.0,>=3.8" groups = ["main"] files = [ - {file = "immutabledict-4.2.2-py3-none-any.whl", hash = "sha256:97c31d098a2c850e93a958badeef765e4736ed7942ec73e439facd764a3a7217"}, - {file = "immutabledict-4.2.2.tar.gz", hash = "sha256:cb6ed3090df593148f94cb407d218ca526fd2639694afdb553dc4f50ce6feeca"}, + {file = "immutabledict-4.3.1-py3-none-any.whl", hash = "sha256:c9facdc0ff30fdb8e35bd16532026cac472a549e182c94fa201b51b25e4bf7bf"}, + {file = "immutabledict-4.3.1.tar.gz", hash = "sha256:f844a669106cfdc73f47b1a9da003782fb17dc955a54c80972e0d93d1c63c514"}, ] [[package]] name = "importlib-metadata" -version = "6.7.0" +version = "8.7.1" description = "Read metadata from Python packages" optional = false -python-versions = ">=3.7" +python-versions = ">=3.9" groups = ["dev"] markers = "python_version < \"3.12\" and platform_machine != \"ppc64le\" and platform_machine != \"s390x\"" files = [ - {file = "importlib_metadata-6.7.0-py3-none-any.whl", hash = "sha256:cb52082e659e97afc5dac71e79de97d8681de3aa07ff18578330904a9d18e5b5"}, - {file = "importlib_metadata-6.7.0.tar.gz", hash = "sha256:1aaf550d4f73e5d6783e7acb77aec43d49da8017410afae93822cc9cca98c4d4"}, + {file = "importlib_metadata-8.7.1-py3-none-any.whl", hash = "sha256:5a1f80bf1daa489495071efbb095d75a634cf28a8bc299581244063b53176151"}, + {file = "importlib_metadata-8.7.1.tar.gz", hash = "sha256:49fef1ae6440c182052f407c8d34a68f72efc36db9ca90dc0113398f2fdde8bb"}, ] [package.dependencies] -zipp = ">=0.5" +zipp = ">=3.20" [package.extras] -docs = ["furo", "jaraco.packaging (>=9)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] +check = ["pytest-checkdocs (>=2.4)", "pytest-ruff (>=0.2.1) ; sys_platform != \"cygwin\""] +cover = ["pytest-cov"] +doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] +enabler = ["pytest-enabler (>=3.4)"] perf = ["ipython"] -testing = ["flufl.flake8", "importlib-resources (>=1.3) ; python_version < \"3.9\"", "packaging", "pyfakefs", "pytest (>=6)", "pytest-black (>=0.3.7) ; platform_python_implementation != \"PyPy\"", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=1.3)", "pytest-mypy (>=0.9.1) ; platform_python_implementation != \"PyPy\"", "pytest-perf (>=0.9.2)", "pytest-ruff"] +test = ["flufl.flake8", "jaraco.test (>=5.4)", "packaging", "pyfakefs", "pytest (>=6,!=8.1.*)", "pytest-perf (>=0.9.2)"] +type = ["mypy (<1.19) ; platform_python_implementation == \"PyPy\"", "pytest-mypy (>=1.0.1)"] [[package]] name = "incremental" -version = "24.7.2" -description = "A small library that versions your Python projects." +version = "24.11.0" +description = "A CalVer version manager that supports the future." optional = false python-versions = ">=3.8" groups = ["main"] files = [ - {file = "incremental-24.7.2-py3-none-any.whl", hash = "sha256:8cb2c3431530bec48ad70513931a760f446ad6c25e8333ca5d95e24b0ed7b8fe"}, - {file = "incremental-24.7.2.tar.gz", hash = "sha256:fb4f1d47ee60efe87d4f6f0ebb5f70b9760db2b2574c59c8e8912be4ebd464c9"}, + {file = "incremental-24.11.0-py3-none-any.whl", hash = "sha256:a34450716b1c4341fe6676a0598e88a39e04189f4dce5dc96f656e040baa10b3"}, + {file = "incremental-24.11.0.tar.gz", hash = "sha256:87d3480dbb083c1d736222511a8cf380012a8176c2456d01ef483242abbbcf8c"}, ] [package.dependencies] -setuptools = ">=61.0" +packaging = ">=17.0" tomli = {version = "*", markers = "python_version < \"3.11\""} -[package.extras] -scripts = ["click (>=6.0)"] - [[package]] name = "jaeger-client" version = "4.8.0" @@ -874,7 +930,7 @@ description = "Jaeger Python OpenTracing Tracer implementation" optional = true python-versions = ">=3.7" groups = ["main"] -markers = "extra == \"all\" or extra == \"opentracing\"" +markers = "extra == \"opentracing\" or extra == \"all\"" files = [ {file = "jaeger-client-4.8.0.tar.gz", hash = "sha256:3157836edab8e2c209bd2d6ae61113db36f7ee399e66b1dcbb715d87ab49bfe0"}, ] @@ -890,40 +946,88 @@ tests = ["codecov", "coverage", "flake8", "flake8-quotes", "flake8-typing-import [[package]] name = "jaraco-classes" -version = "3.2.3" +version = "3.4.0" description = "Utility functions for Python class constructs" optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" groups = ["dev"] markers = "platform_machine != \"ppc64le\" and platform_machine != \"s390x\"" files = [ - {file = "jaraco.classes-3.2.3-py3-none-any.whl", hash = "sha256:2353de3288bc6b82120752201c6b1c1a14b058267fa424ed5ce5984e3b922158"}, - {file = "jaraco.classes-3.2.3.tar.gz", hash = "sha256:89559fa5c1d3c34eff6f631ad80bb21f378dbcbb35dd161fd2c6b93f5be2f98a"}, + {file = "jaraco.classes-3.4.0-py3-none-any.whl", hash = "sha256:f662826b6bed8cace05e7ff873ce0f9283b5c924470fe664fff1c2f00f581790"}, + {file = "jaraco.classes-3.4.0.tar.gz", hash = "sha256:47a024b51d0239c0dd8c8540c6c7f484be3b8fcf0b2d85c13825780d3b3f3acd"}, ] [package.dependencies] more-itertools = "*" [package.extras] -docs = ["jaraco.packaging (>=9)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)"] -testing = ["flake8 (<5)", "pytest (>=6)", "pytest-black (>=0.3.7) ; platform_python_implementation != \"PyPy\"", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=1.3)", "pytest-flake8", "pytest-mypy (>=0.9.1) ; platform_python_implementation != \"PyPy\""] +docs = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] +testing = ["pytest (>=6)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-mypy", "pytest-ruff (>=0.2.1)"] + +[[package]] +name = "jaraco-context" +version = "6.1.0" +description = "Useful decorators and context managers" +optional = false +python-versions = ">=3.9" +groups = ["dev"] +markers = "platform_machine != \"ppc64le\" and platform_machine != \"s390x\"" +files = [ + {file = "jaraco_context-6.1.0-py3-none-any.whl", hash = "sha256:a43b5ed85815223d0d3cfdb6d7ca0d2bc8946f28f30b6f3216bda070f68badda"}, + {file = "jaraco_context-6.1.0.tar.gz", hash = "sha256:129a341b0a85a7db7879e22acd66902fda67882db771754574338898b2d5d86f"}, +] + +[package.dependencies] +"backports.tarfile" = {version = "*", markers = "python_version < \"3.12\""} + +[package.extras] +check = ["pytest-checkdocs (>=2.4)", "pytest-ruff (>=0.2.1) ; sys_platform != \"cygwin\""] +cover = ["pytest-cov"] +doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] +enabler = ["pytest-enabler (>=3.4)"] +test = ["jaraco.test (>=5.6.0)", "portend", "pytest (>=6,!=8.1.*)"] +type = ["mypy (<1.19) ; platform_python_implementation == \"PyPy\"", "pytest-mypy (>=1.0.1)"] + +[[package]] +name = "jaraco-functools" +version = "4.4.0" +description = "Functools like those found in stdlib" +optional = false +python-versions = ">=3.9" +groups = ["dev"] +markers = "platform_machine != \"ppc64le\" and platform_machine != \"s390x\"" +files = [ + {file = "jaraco_functools-4.4.0-py3-none-any.whl", hash = "sha256:9eec1e36f45c818d9bf307c8948eb03b2b56cd44087b3cdc989abca1f20b9176"}, + {file = "jaraco_functools-4.4.0.tar.gz", hash = "sha256:da21933b0417b89515562656547a77b4931f98176eb173644c0d35032a33d6bb"}, +] + +[package.dependencies] +more_itertools = "*" + +[package.extras] +check = ["pytest-checkdocs (>=2.4)", "pytest-ruff (>=0.2.1) ; sys_platform != \"cygwin\""] +cover = ["pytest-cov"] +doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] +enabler = ["pytest-enabler (>=3.4)"] +test = ["jaraco.classes", "pytest (>=6,!=8.1.*)"] +type = ["mypy (<1.19) ; platform_python_implementation == \"PyPy\"", "pytest-mypy (>=1.0.1)"] [[package]] name = "jeepney" -version = "0.8.0" +version = "0.9.0" description = "Low-level, pure Python DBus protocol wrapper." optional = false python-versions = ">=3.7" groups = ["dev"] markers = "platform_machine != \"ppc64le\" and platform_machine != \"s390x\" and sys_platform == \"linux\"" files = [ - {file = "jeepney-0.8.0-py3-none-any.whl", hash = "sha256:c0a454ad016ca575060802ee4d590dd912e35c122fa04e70306de3d076cce755"}, - {file = "jeepney-0.8.0.tar.gz", hash = "sha256:5efe48d255973902f6badc3ce55e2aa6c5c3b3bc642059ef3a91247bcfcc5806"}, + {file = "jeepney-0.9.0-py3-none-any.whl", hash = "sha256:97e5714520c16fc0a45695e5365a2e11b81ea79bba796e26f9f1d178cb182683"}, + {file = "jeepney-0.9.0.tar.gz", hash = "sha256:cf0e9e845622b81e4a28df94c40345400256ec608d0e55bb8a3feaa9163f5732"}, ] [package.extras] -test = ["async-timeout", "pytest", "pytest-asyncio (>=0.17)", "pytest-trio", "testpath", "trio"] -trio = ["async_generator ; python_version == \"3.6\"", "trio"] +test = ["async-timeout ; python_version < \"3.11\"", "pytest", "pytest-asyncio (>=0.17)", "pytest-trio", "testpath", "trio"] +trio = ["trio"] [[package]] name = "jinja2" @@ -945,21 +1049,21 @@ i18n = ["Babel (>=2.7)"] [[package]] name = "jsonschema" -version = "4.25.1" +version = "4.26.0" description = "An implementation of JSON Schema validation for Python" optional = false -python-versions = ">=3.9" +python-versions = ">=3.10" groups = ["main"] files = [ - {file = "jsonschema-4.25.1-py3-none-any.whl", hash = "sha256:3fba0169e345c7175110351d456342c364814cfcf3b964ba4587f22915230a63"}, - {file = "jsonschema-4.25.1.tar.gz", hash = "sha256:e4a9655ce0da0c0b67a085847e00a3a51449e1157f4f75e9fb5aa545e122eb85"}, + {file = "jsonschema-4.26.0-py3-none-any.whl", hash = "sha256:d489f15263b8d200f8387e64b4c3a75f06629559fb73deb8fdfb525f2dab50ce"}, + {file = "jsonschema-4.26.0.tar.gz", hash = "sha256:0c26707e2efad8aa1bfc5b7ce170f3fccc2e4918ff85989ba9ffa9facb2be326"}, ] [package.dependencies] attrs = ">=22.2.0" jsonschema-specifications = ">=2023.03.6" referencing = ">=0.28.4" -rpds-py = ">=0.7.1" +rpds-py = ">=0.25.0" [package.extras] format = ["fqdn", "idna", "isoduration", "jsonpointer (>1.13)", "rfc3339-validator", "rfc3987", "uri-template", "webcolors (>=1.11)"] @@ -967,43 +1071,49 @@ format-nongpl = ["fqdn", "idna", "isoduration", "jsonpointer (>1.13)", "rfc3339- [[package]] name = "jsonschema-specifications" -version = "2023.7.1" +version = "2025.9.1" description = "The JSON Schema meta-schemas and vocabularies, exposed as a Registry" optional = false -python-versions = ">=3.8" +python-versions = ">=3.9" groups = ["main"] files = [ - {file = "jsonschema_specifications-2023.7.1-py3-none-any.whl", hash = "sha256:05adf340b659828a004220a9613be00fa3f223f2b82002e273dee62fd50524b1"}, - {file = "jsonschema_specifications-2023.7.1.tar.gz", hash = "sha256:c91a50404e88a1f6ba40636778e2ee08f6e24c5613fe4c53ac24578a5a7f72bb"}, + {file = "jsonschema_specifications-2025.9.1-py3-none-any.whl", hash = "sha256:98802fee3a11ee76ecaca44429fda8a41bff98b00a0f2838151b113f210cc6fe"}, + {file = "jsonschema_specifications-2025.9.1.tar.gz", hash = "sha256:b540987f239e745613c7a9176f3edb72b832a4ac465cf02712288397832b5e8d"}, ] [package.dependencies] -referencing = ">=0.28.0" +referencing = ">=0.31.0" [[package]] name = "keyring" -version = "23.13.1" +version = "25.7.0" description = "Store and access your passwords safely." optional = false -python-versions = ">=3.7" +python-versions = ">=3.9" groups = ["dev"] markers = "platform_machine != \"ppc64le\" and platform_machine != \"s390x\"" files = [ - {file = "keyring-23.13.1-py3-none-any.whl", hash = "sha256:771ed2a91909389ed6148631de678f82ddc73737d85a927f382a8a1b157898cd"}, - {file = "keyring-23.13.1.tar.gz", hash = "sha256:ba2e15a9b35e21908d0aaf4e0a47acc52d6ae33444df0da2b49d41a46ef6d678"}, + {file = "keyring-25.7.0-py3-none-any.whl", hash = "sha256:be4a0b195f149690c166e850609a477c532ddbfbaed96a404d4e43f8d5e2689f"}, + {file = "keyring-25.7.0.tar.gz", hash = "sha256:fe01bd85eb3f8fb3dd0405defdeac9a5b4f6f0439edbb3149577f244a2e8245b"}, ] [package.dependencies] -importlib-metadata = {version = ">=4.11.4", markers = "python_version < \"3.12\""} +importlib_metadata = {version = ">=4.11.4", markers = "python_version < \"3.12\""} "jaraco.classes" = "*" +"jaraco.context" = "*" +"jaraco.functools" = "*" jeepney = {version = ">=0.4.2", markers = "sys_platform == \"linux\""} pywin32-ctypes = {version = ">=0.2.0", markers = "sys_platform == \"win32\""} SecretStorage = {version = ">=3.2", markers = "sys_platform == \"linux\""} [package.extras] -completion = ["shtab"] -docs = ["furo", "jaraco.packaging (>=9)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)"] -testing = ["flake8 (<5)", "pytest (>=6)", "pytest-black (>=0.3.7) ; platform_python_implementation != \"PyPy\"", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=1.3)", "pytest-flake8 ; python_version < \"3.12\"", "pytest-mypy (>=0.9.1) ; platform_python_implementation != \"PyPy\""] +check = ["pytest-checkdocs (>=2.4)", "pytest-ruff (>=0.2.1) ; sys_platform != \"cygwin\""] +completion = ["shtab (>=1.1.0)"] +cover = ["pytest-cov"] +doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] +enabler = ["pytest-enabler (>=3.4)"] +test = ["pyfakefs", "pytest (>=6,!=8.1.*)"] +type = ["pygobject-stubs", "pytest-mypy (>=1.0.1)", "shtab", "types-pywin32"] [[package]] name = "ldap3" @@ -1012,7 +1122,7 @@ description = "A strictly RFC 4510 conforming LDAP V3 pure Python client library optional = true python-versions = "*" groups = ["main"] -markers = "extra == \"all\" or extra == \"matrix-synapse-ldap3\"" +markers = "extra == \"matrix-synapse-ldap3\" or extra == \"all\"" files = [ {file = "ldap3-2.9.1-py2.py3-none-any.whl", hash = "sha256:5869596fc4948797020d3f03b7939da938778a0f9e2009f7a072ccf92b8e8d70"}, {file = "ldap3-2.9.1.tar.gz", hash = "sha256:f3e7fc4718e3f09dda568b57100095e0ce58633bcabbed8667ce3f8fbaa4229f"}, @@ -1023,88 +1133,103 @@ pyasn1 = ">=0.4.6" [[package]] name = "librt" -version = "0.6.3" +version = "0.8.1" description = "Mypyc runtime library" optional = false python-versions = ">=3.9" groups = ["dev"] -files = [ - {file = "librt-0.6.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:45660d26569cc22ed30adf583389d8a0d1b468f8b5e518fcf9bfe2cd298f9dd1"}, - {file = "librt-0.6.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:54f3b2177fb892d47f8016f1087d21654b44f7fc4cf6571c1c6b3ea531ab0fcf"}, - {file = "librt-0.6.3-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:c5b31bed2c2f2fa1fcb4815b75f931121ae210dc89a3d607fb1725f5907f1437"}, - {file = "librt-0.6.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8f8ed5053ef9fb08d34f1fd80ff093ccbd1f67f147633a84cf4a7d9b09c0f089"}, - {file = "librt-0.6.3-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3f0e4bd9bcb0ee34fa3dbedb05570da50b285f49e52c07a241da967840432513"}, - {file = "librt-0.6.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:d8f89c8d20dfa648a3f0a56861946eb00e5b00d6b00eea14bc5532b2fcfa8ef1"}, - {file = "librt-0.6.3-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:ecc2c526547eacd20cb9fbba19a5268611dbc70c346499656d6cf30fae328977"}, - {file = "librt-0.6.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:fbedeb9b48614d662822ee514567d2d49a8012037fc7b4cd63f282642c2f4b7d"}, - {file = "librt-0.6.3-cp310-cp310-win32.whl", hash = "sha256:0765b0fe0927d189ee14b087cd595ae636bef04992e03fe6dfdaa383866c8a46"}, - {file = "librt-0.6.3-cp310-cp310-win_amd64.whl", hash = "sha256:8c659f9fb8a2f16dc4131b803fa0144c1dadcb3ab24bb7914d01a6da58ae2457"}, - {file = "librt-0.6.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:61348cc488b18d1b1ff9f3e5fcd5ac43ed22d3e13e862489d2267c2337285c08"}, - {file = "librt-0.6.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:64645b757d617ad5f98c08e07620bc488d4bced9ced91c6279cec418f16056fa"}, - {file = "librt-0.6.3-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:26b8026393920320bb9a811b691d73c5981385d537ffc5b6e22e53f7b65d4122"}, - {file = "librt-0.6.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d998b432ed9ffccc49b820e913c8f327a82026349e9c34fa3690116f6b70770f"}, - {file = "librt-0.6.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e18875e17ef69ba7dfa9623f2f95f3eda6f70b536079ee6d5763ecdfe6cc9040"}, - {file = "librt-0.6.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:a218f85081fc3f70cddaed694323a1ad7db5ca028c379c214e3a7c11c0850523"}, - {file = "librt-0.6.3-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:1ef42ff4edd369e84433ce9b188a64df0837f4f69e3d34d3b34d4955c599d03f"}, - {file = "librt-0.6.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:0e0f2b79993fec23a685b3e8107ba5f8675eeae286675a216da0b09574fa1e47"}, - {file = "librt-0.6.3-cp311-cp311-win32.whl", hash = "sha256:fd98cacf4e0fabcd4005c452cb8a31750258a85cab9a59fb3559e8078da408d7"}, - {file = "librt-0.6.3-cp311-cp311-win_amd64.whl", hash = "sha256:e17b5b42c8045867ca9d1f54af00cc2275198d38de18545edaa7833d7e9e4ac8"}, - {file = "librt-0.6.3-cp311-cp311-win_arm64.whl", hash = "sha256:87597e3d57ec0120a3e1d857a708f80c02c42ea6b00227c728efbc860f067c45"}, - {file = "librt-0.6.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:74418f718083009108dc9a42c21bf2e4802d49638a1249e13677585fcc9ca176"}, - {file = "librt-0.6.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:514f3f363d1ebc423357d36222c37e5c8e6674b6eae8d7195ac9a64903722057"}, - {file = "librt-0.6.3-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:cf1115207a5049d1f4b7b4b72de0e52f228d6c696803d94843907111cbf80610"}, - {file = "librt-0.6.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ad8ba80cdcea04bea7b78fcd4925bfbf408961e9d8397d2ee5d3ec121e20c08c"}, - {file = "librt-0.6.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4018904c83eab49c814e2494b4e22501a93cdb6c9f9425533fe693c3117126f9"}, - {file = "librt-0.6.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8983c5c06ac9c990eac5eb97a9f03fe41dc7e9d7993df74d9e8682a1056f596c"}, - {file = "librt-0.6.3-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:d7769c579663a6f8dbf34878969ac71befa42067ce6bf78e6370bf0d1194997c"}, - {file = "librt-0.6.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:d3c9a07eafdc70556f8c220da4a538e715668c0c63cabcc436a026e4e89950bf"}, - {file = "librt-0.6.3-cp312-cp312-win32.whl", hash = "sha256:38320386a48a15033da295df276aea93a92dfa94a862e06893f75ea1d8bbe89d"}, - {file = "librt-0.6.3-cp312-cp312-win_amd64.whl", hash = "sha256:c0ecf4786ad0404b072196b5df774b1bb23c8aacdcacb6c10b4128bc7b00bd01"}, - {file = "librt-0.6.3-cp312-cp312-win_arm64.whl", hash = "sha256:9f2a6623057989ebc469cd9cc8fe436c40117a0147627568d03f84aef7854c55"}, - {file = "librt-0.6.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:9e716f9012148a81f02f46a04fc4c663420c6fbfeacfac0b5e128cf43b4413d3"}, - {file = "librt-0.6.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:669ff2495728009a96339c5ad2612569c6d8be4474e68f3f3ac85d7c3261f5f5"}, - {file = "librt-0.6.3-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:349b6873ebccfc24c9efd244e49da9f8a5c10f60f07575e248921aae2123fc42"}, - {file = "librt-0.6.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0c74c26736008481c9f6d0adf1aedb5a52aff7361fea98276d1f965c0256ee70"}, - {file = "librt-0.6.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:408a36ddc75e91918cb15b03460bdc8a015885025d67e68c6f78f08c3a88f522"}, - {file = "librt-0.6.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:e61ab234624c9ffca0248a707feffe6fac2343758a36725d8eb8a6efef0f8c30"}, - {file = "librt-0.6.3-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:324462fe7e3896d592b967196512491ec60ca6e49c446fe59f40743d08c97917"}, - {file = "librt-0.6.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:36b2ec8c15030002c7f688b4863e7be42820d7c62d9c6eece3db54a2400f0530"}, - {file = "librt-0.6.3-cp313-cp313-win32.whl", hash = "sha256:25b1b60cb059471c0c0c803e07d0dfdc79e41a0a122f288b819219ed162672a3"}, - {file = "librt-0.6.3-cp313-cp313-win_amd64.whl", hash = "sha256:10a95ad074e2a98c9e4abc7f5b7d40e5ecbfa84c04c6ab8a70fabf59bd429b88"}, - {file = "librt-0.6.3-cp313-cp313-win_arm64.whl", hash = "sha256:17000df14f552e86877d67e4ab7966912224efc9368e998c96a6974a8d609bf9"}, - {file = "librt-0.6.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:8e695f25d1a425ad7a272902af8ab8c8d66c1998b177e4b5f5e7b4e215d0c88a"}, - {file = "librt-0.6.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:3e84a4121a7ae360ca4da436548a9c1ca8ca134a5ced76c893cc5944426164bd"}, - {file = "librt-0.6.3-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:05f385a414de3f950886ea0aad8f109650d4b712cf9cc14cc17f5f62a9ab240b"}, - {file = "librt-0.6.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:36a8e337461150b05ca2c7bdedb9e591dfc262c5230422cea398e89d0c746cdc"}, - {file = "librt-0.6.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:dcbe48f6a03979384f27086484dc2a14959be1613cb173458bd58f714f2c48f3"}, - {file = "librt-0.6.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:4bca9e4c260233fba37b15c4ec2f78aa99c1a79fbf902d19dd4a763c5c3fb751"}, - {file = "librt-0.6.3-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:760c25ed6ac968e24803eb5f7deb17ce026902d39865e83036bacbf5cf242aa8"}, - {file = "librt-0.6.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:4aa4a93a353ccff20df6e34fa855ae8fd788832c88f40a9070e3ddd3356a9f0e"}, - {file = "librt-0.6.3-cp314-cp314-win32.whl", hash = "sha256:cb92741c2b4ea63c09609b064b26f7f5d9032b61ae222558c55832ec3ad0bcaf"}, - {file = "librt-0.6.3-cp314-cp314-win_amd64.whl", hash = "sha256:fdcd095b1b812d756fa5452aca93b962cf620694c0cadb192cec2bb77dcca9a2"}, - {file = "librt-0.6.3-cp314-cp314-win_arm64.whl", hash = "sha256:822ca79e28720a76a935c228d37da6579edef048a17cd98d406a2484d10eda78"}, - {file = "librt-0.6.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:078cd77064d1640cb7b0650871a772956066174d92c8aeda188a489b58495179"}, - {file = "librt-0.6.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:5cc22f7f5c0cc50ed69f4b15b9c51d602aabc4500b433aaa2ddd29e578f452f7"}, - {file = "librt-0.6.3-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:14b345eb7afb61b9fdcdfda6738946bd11b8e0f6be258666b0646af3b9bb5916"}, - {file = "librt-0.6.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6d46aa46aa29b067f0b8b84f448fd9719aaf5f4c621cc279164d76a9dc9ab3e8"}, - {file = "librt-0.6.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1b51ba7d9d5d9001494769eca8c0988adce25d0a970c3ba3f2eb9df9d08036fc"}, - {file = "librt-0.6.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:ced0925a18fddcff289ef54386b2fc230c5af3c83b11558571124bfc485b8c07"}, - {file = "librt-0.6.3-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:6bac97e51f66da2ca012adddbe9fd656b17f7368d439de30898f24b39512f40f"}, - {file = "librt-0.6.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:b2922a0e8fa97395553c304edc3bd36168d8eeec26b92478e292e5d4445c1ef0"}, - {file = "librt-0.6.3-cp314-cp314t-win32.whl", hash = "sha256:f33462b19503ba68d80dac8a1354402675849259fb3ebf53b67de86421735a3a"}, - {file = "librt-0.6.3-cp314-cp314t-win_amd64.whl", hash = "sha256:04f8ce401d4f6380cfc42af0f4e67342bf34c820dae01343f58f472dbac75dcf"}, - {file = "librt-0.6.3-cp314-cp314t-win_arm64.whl", hash = "sha256:afb39550205cc5e5c935762c6bf6a2bb34f7d21a68eadb25e2db7bf3593fecc0"}, - {file = "librt-0.6.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:09262cb2445b6f15d09141af20b95bb7030c6f13b00e876ad8fdd1a9045d6aa5"}, - {file = "librt-0.6.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:57705e8eec76c5b77130d729c0f70190a9773366c555c5457c51eace80afd873"}, - {file = "librt-0.6.3-cp39-cp39-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:3ac2a7835434b31def8ed5355dd9b895bbf41642d61967522646d1d8b9681106"}, - {file = "librt-0.6.3-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:71f0a5918aebbea1e7db2179a8fe87e8a8732340d9e8b8107401fb407eda446e"}, - {file = "librt-0.6.3-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:aa346e202e6e1ebc01fe1c69509cffe486425884b96cb9ce155c99da1ecbe0e9"}, - {file = "librt-0.6.3-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:92267f865c7bbd12327a0d394666948b9bf4b51308b52947c0cc453bfa812f5d"}, - {file = "librt-0.6.3-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:86605d5bac340beb030cbc35859325982a79047ebdfba1e553719c7126a2389d"}, - {file = "librt-0.6.3-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:98e4bbecbef8d2a60ecf731d735602feee5ac0b32117dbbc765e28b054bac912"}, - {file = "librt-0.6.3-cp39-cp39-win32.whl", hash = "sha256:3caa0634c02d5ff0b2ae4a28052e0d8c5f20d497623dc13f629bd4a9e2a6efad"}, - {file = "librt-0.6.3-cp39-cp39-win_amd64.whl", hash = "sha256:b47395091e7e0ece1e6ebac9b98bf0c9084d1e3d3b2739aa566be7e56e3f7bf2"}, - {file = "librt-0.6.3.tar.gz", hash = "sha256:c724a884e642aa2bbad52bb0203ea40406ad742368a5f90da1b220e970384aae"}, +markers = "platform_python_implementation != \"PyPy\"" +files = [ + {file = "librt-0.8.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:81fd938344fecb9373ba1b155968c8a329491d2ce38e7ddb76f30ffb938f12dc"}, + {file = "librt-0.8.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:5db05697c82b3a2ec53f6e72b2ed373132b0c2e05135f0696784e97d7f5d48e7"}, + {file = "librt-0.8.1-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:d56bc4011975f7460bea7b33e1ff425d2f1adf419935ff6707273c77f8a4ada6"}, + {file = "librt-0.8.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5cdc0f588ff4b663ea96c26d2a230c525c6fc62b28314edaaaca8ed5af931ad0"}, + {file = "librt-0.8.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:97c2b54ff6717a7a563b72627990bec60d8029df17df423f0ed37d56a17a176b"}, + {file = "librt-0.8.1-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:8f1125e6bbf2f1657d9a2f3ccc4a2c9b0c8b176965bb565dd4d86be67eddb4b6"}, + {file = "librt-0.8.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:8f4bb453f408137d7581be309b2fbc6868a80e7ef60c88e689078ee3a296ae71"}, + {file = "librt-0.8.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:c336d61d2fe74a3195edc1646d53ff1cddd3a9600b09fa6ab75e5514ba4862a7"}, + {file = "librt-0.8.1-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:eb5656019db7c4deacf0c1a55a898c5bb8f989be904597fcb5232a2f4828fa05"}, + {file = "librt-0.8.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:c25d9e338d5bed46c1632f851babf3d13c78f49a225462017cf5e11e845c5891"}, + {file = "librt-0.8.1-cp310-cp310-win32.whl", hash = "sha256:aaab0e307e344cb28d800957ef3ec16605146ef0e59e059a60a176d19543d1b7"}, + {file = "librt-0.8.1-cp310-cp310-win_amd64.whl", hash = "sha256:56e04c14b696300d47b3bc5f1d10a00e86ae978886d0cee14e5714fafb5df5d2"}, + {file = "librt-0.8.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:681dc2451d6d846794a828c16c22dc452d924e9f700a485b7ecb887a30aad1fd"}, + {file = "librt-0.8.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:a3b4350b13cc0e6f5bec8fa7caf29a8fb8cdc051a3bae45cfbfd7ce64f009965"}, + {file = "librt-0.8.1-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:ac1e7817fd0ed3d14fd7c5df91daed84c48e4c2a11ee99c0547f9f62fdae13da"}, + {file = "librt-0.8.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:747328be0c5b7075cde86a0e09d7a9196029800ba75a1689332348e998fb85c0"}, + {file = "librt-0.8.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f0af2bd2bc204fa27f3d6711d0f360e6b8c684a035206257a81673ab924aa11e"}, + {file = "librt-0.8.1-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:d480de377f5b687b6b1bc0c0407426da556e2a757633cc7e4d2e1a057aa688f3"}, + {file = "librt-0.8.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d0ee06b5b5291f609ddb37b9750985b27bc567791bc87c76a569b3feed8481ac"}, + {file = "librt-0.8.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:9e2c6f77b9ad48ce5603b83b7da9ee3e36b3ab425353f695cba13200c5d96596"}, + {file = "librt-0.8.1-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:439352ba9373f11cb8e1933da194dcc6206daf779ff8df0ed69c5e39113e6a99"}, + {file = "librt-0.8.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:82210adabbc331dbb65d7868b105185464ef13f56f7f76688565ad79f648b0fe"}, + {file = "librt-0.8.1-cp311-cp311-win32.whl", hash = "sha256:52c224e14614b750c0a6d97368e16804a98c684657c7518752c356834fff83bb"}, + {file = "librt-0.8.1-cp311-cp311-win_amd64.whl", hash = "sha256:c00e5c884f528c9932d278d5c9cbbea38a6b81eb62c02e06ae53751a83a4d52b"}, + {file = "librt-0.8.1-cp311-cp311-win_arm64.whl", hash = "sha256:f7cdf7f26c2286ffb02e46d7bac56c94655540b26347673bea15fa52a6af17e9"}, + {file = "librt-0.8.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:a28f2612ab566b17f3698b0da021ff9960610301607c9a5e8eaca62f5e1c350a"}, + {file = "librt-0.8.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:60a78b694c9aee2a0f1aaeaa7d101cf713e92e8423a941d2897f4fa37908dab9"}, + {file = "librt-0.8.1-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:758509ea3f1eba2a57558e7e98f4659d0ea7670bff49673b0dde18a3c7e6c0eb"}, + {file = "librt-0.8.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:039b9f2c506bd0ab0f8725aa5ba339c6f0cd19d3b514b50d134789809c24285d"}, + {file = "librt-0.8.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5bb54f1205a3a6ab41a6fd71dfcdcbd278670d3a90ca502a30d9da583105b6f7"}, + {file = "librt-0.8.1-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:05bd41cdee35b0c59c259f870f6da532a2c5ca57db95b5f23689fcb5c9e42440"}, + {file = "librt-0.8.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:adfab487facf03f0d0857b8710cf82d0704a309d8ffc33b03d9302b4c64e91a9"}, + {file = "librt-0.8.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:153188fe98a72f206042be10a2c6026139852805215ed9539186312d50a8e972"}, + {file = "librt-0.8.1-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:dd3c41254ee98604b08bd5b3af5bf0a89740d4ee0711de95b65166bf44091921"}, + {file = "librt-0.8.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e0d138c7ae532908cbb342162b2611dbd4d90c941cd25ab82084aaf71d2c0bd0"}, + {file = "librt-0.8.1-cp312-cp312-win32.whl", hash = "sha256:43353b943613c5d9c49a25aaffdba46f888ec354e71e3529a00cca3f04d66a7a"}, + {file = "librt-0.8.1-cp312-cp312-win_amd64.whl", hash = "sha256:ff8baf1f8d3f4b6b7257fcb75a501f2a5499d0dda57645baa09d4d0d34b19444"}, + {file = "librt-0.8.1-cp312-cp312-win_arm64.whl", hash = "sha256:0f2ae3725904f7377e11cc37722d5d401e8b3d5851fb9273d7f4fe04f6b3d37d"}, + {file = "librt-0.8.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:7e6bad1cd94f6764e1e21950542f818a09316645337fd5ab9a7acc45d99a8f35"}, + {file = "librt-0.8.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:cf450f498c30af55551ba4f66b9123b7185362ec8b625a773b3d39aa1a717583"}, + {file = "librt-0.8.1-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:eca45e982fa074090057132e30585a7e8674e9e885d402eae85633e9f449ce6c"}, + {file = "librt-0.8.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0c3811485fccfda840861905b8c70bba5ec094e02825598bb9d4ca3936857a04"}, + {file = "librt-0.8.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5e4af413908f77294605e28cfd98063f54b2c790561383971d2f52d113d9c363"}, + {file = "librt-0.8.1-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:5212a5bd7fae98dae95710032902edcd2ec4dc994e883294f75c857b83f9aba0"}, + {file = "librt-0.8.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:e692aa2d1d604e6ca12d35e51fdc36f4cda6345e28e36374579f7ef3611b3012"}, + {file = "librt-0.8.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:4be2a5c926b9770c9e08e717f05737a269b9d0ebc5d2f0060f0fe3fe9ce47acb"}, + {file = "librt-0.8.1-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:fd1a720332ea335ceb544cf0a03f81df92abd4bb887679fd1e460976b0e6214b"}, + {file = "librt-0.8.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:93c2af9e01e0ef80d95ae3c720be101227edae5f2fe7e3dc63d8857fadfc5a1d"}, + {file = "librt-0.8.1-cp313-cp313-win32.whl", hash = "sha256:086a32dbb71336627e78cc1d6ee305a68d038ef7d4c39aaff41ae8c9aa46e91a"}, + {file = "librt-0.8.1-cp313-cp313-win_amd64.whl", hash = "sha256:e11769a1dbda4da7b00a76cfffa67aa47cfa66921d2724539eee4b9ede780b79"}, + {file = "librt-0.8.1-cp313-cp313-win_arm64.whl", hash = "sha256:924817ab3141aca17893386ee13261f1d100d1ef410d70afe4389f2359fea4f0"}, + {file = "librt-0.8.1-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:6cfa7fe54fd4d1f47130017351a959fe5804bda7a0bc7e07a2cdbc3fdd28d34f"}, + {file = "librt-0.8.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:228c2409c079f8c11fb2e5d7b277077f694cb93443eb760e00b3b83cb8b3176c"}, + {file = "librt-0.8.1-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:7aae78ab5e3206181780e56912d1b9bb9f90a7249ce12f0e8bf531d0462dd0fc"}, + {file = "librt-0.8.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:172d57ec04346b047ca6af181e1ea4858086c80bdf455f61994c4aa6fc3f866c"}, + {file = "librt-0.8.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6b1977c4ea97ce5eb7755a78fae68d87e4102e4aaf54985e8b56806849cc06a3"}, + {file = "librt-0.8.1-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:10c42e1f6fd06733ef65ae7bebce2872bcafd8d6e6b0a08fe0a05a23b044fb14"}, + {file = "librt-0.8.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:4c8dfa264b9193c4ee19113c985c95f876fae5e51f731494fc4e0cf594990ba7"}, + {file = "librt-0.8.1-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:01170b6729a438f0dedc4a26ed342e3dc4f02d1000b4b19f980e1877f0c297e6"}, + {file = "librt-0.8.1-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:7b02679a0d783bdae30d443025b94465d8c3dc512f32f5b5031f93f57ac32071"}, + {file = "librt-0.8.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:190b109bb69592a3401fe1ffdea41a2e73370ace2ffdc4a0e8e2b39cdea81b78"}, + {file = "librt-0.8.1-cp314-cp314-win32.whl", hash = "sha256:e70a57ecf89a0f64c24e37f38d3fe217a58169d2fe6ed6d70554964042474023"}, + {file = "librt-0.8.1-cp314-cp314-win_amd64.whl", hash = "sha256:7e2f3edca35664499fbb36e4770650c4bd4a08abc1f4458eab9df4ec56389730"}, + {file = "librt-0.8.1-cp314-cp314-win_arm64.whl", hash = "sha256:0d2f82168e55ddefd27c01c654ce52379c0750ddc31ee86b4b266bcf4d65f2a3"}, + {file = "librt-0.8.1-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:2c74a2da57a094bd48d03fa5d196da83d2815678385d2978657499063709abe1"}, + {file = "librt-0.8.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:a355d99c4c0d8e5b770313b8b247411ed40949ca44e33e46a4789b9293a907ee"}, + {file = "librt-0.8.1-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:2eb345e8b33fb748227409c9f1233d4df354d6e54091f0e8fc53acdb2ffedeb7"}, + {file = "librt-0.8.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9be2f15e53ce4e83cc08adc29b26fb5978db62ef2a366fbdf716c8a6c8901040"}, + {file = "librt-0.8.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:785ae29c1f5c6e7c2cde2c7c0e148147f4503da3abc5d44d482068da5322fd9e"}, + {file = "librt-0.8.1-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:1d3a7da44baf692f0c6aeb5b2a09c5e6fc7a703bca9ffa337ddd2e2da53f7732"}, + {file = "librt-0.8.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:5fc48998000cbc39ec0d5311312dda93ecf92b39aaf184c5e817d5d440b29624"}, + {file = "librt-0.8.1-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:e96baa6820280077a78244b2e06e416480ed859bbd8e5d641cf5742919d8beb4"}, + {file = "librt-0.8.1-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:31362dbfe297b23590530007062c32c6f6176f6099646bb2c95ab1b00a57c382"}, + {file = "librt-0.8.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:cc3656283d11540ab0ea01978378e73e10002145117055e03722417aeab30994"}, + {file = "librt-0.8.1-cp314-cp314t-win32.whl", hash = "sha256:738f08021b3142c2918c03692608baed43bc51144c29e35807682f8070ee2a3a"}, + {file = "librt-0.8.1-cp314-cp314t-win_amd64.whl", hash = "sha256:89815a22daf9c51884fb5dbe4f1ef65ee6a146e0b6a8df05f753e2e4a9359bf4"}, + {file = "librt-0.8.1-cp314-cp314t-win_arm64.whl", hash = "sha256:bf512a71a23504ed08103a13c941f763db13fb11177beb3d9244c98c29fb4a61"}, + {file = "librt-0.8.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:3dff3d3ca8db20e783b1bc7de49c0a2ab0b8387f31236d6a026597d07fcd68ac"}, + {file = "librt-0.8.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:08eec3a1fc435f0d09c87b6bf1ec798986a3544f446b864e4099633a56fcd9ed"}, + {file = "librt-0.8.1-cp39-cp39-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:e3f0a41487fd5fad7e760b9e8a90e251e27c2816fbc2cff36a22a0e6bcbbd9dd"}, + {file = "librt-0.8.1-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bacdb58d9939d95cc557b4dbaa86527c9db2ac1ed76a18bc8d26f6dc8647d851"}, + {file = "librt-0.8.1-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b6d7ab1f01aa753188605b09a51faa44a3327400b00b8cce424c71910fc0a128"}, + {file = "librt-0.8.1-cp39-cp39-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:4998009e7cb9e896569f4be7004f09d0ed70d386fa99d42b6d363f6d200501ac"}, + {file = "librt-0.8.1-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:2cc68eeeef5e906839c7bb0815748b5b0a974ec27125beefc0f942715785b551"}, + {file = "librt-0.8.1-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:0bf69d79a23f4f40b8673a947a234baeeb133b5078b483b7297c5916539cf5d5"}, + {file = "librt-0.8.1-cp39-cp39-musllinux_1_2_riscv64.whl", hash = "sha256:22b46eabd76c1986ee7d231b0765ad387d7673bbd996aa0d0d054b38ac65d8f6"}, + {file = "librt-0.8.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:237796479f4d0637d6b9cbcb926ff424a97735e68ade6facf402df4ec93375ed"}, + {file = "librt-0.8.1-cp39-cp39-win32.whl", hash = "sha256:4beb04b8c66c6ae62f8c1e0b2f097c1ebad9295c929a8d5286c05eae7c2fc7dc"}, + {file = "librt-0.8.1-cp39-cp39-win_amd64.whl", hash = "sha256:64548cde61b692dc0dc379f4b5f59a2f582c2ebe7890d09c1ae3b9e66fa015b7"}, + {file = "librt-0.8.1.tar.gz", hash = "sha256:be46a14693955b3bd96014ccbdb8339ee8c9346fbe11c1b78901b55125f14c73"}, ] [[package]] @@ -1114,7 +1239,7 @@ description = "Powerful and Pythonic XML processing library combining libxml2/li optional = true python-versions = ">=3.8" groups = ["main"] -markers = "extra == \"all\" or extra == \"url-preview\"" +markers = "extra == \"url-preview\" or extra == \"all\"" 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"}, @@ -1305,73 +1430,101 @@ testing = ["coverage", "pytest", "pytest-cov", "pytest-regressions", "requests"] [[package]] name = "markupsafe" -version = "3.0.2" +version = "3.0.3" description = "Safely add untrusted strings to HTML/XML markup." optional = false python-versions = ">=3.9" groups = ["main", "dev"] files = [ - {file = "MarkupSafe-3.0.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:7e94c425039cde14257288fd61dcfb01963e658efbc0ff54f5306b06054700f8"}, - {file = "MarkupSafe-3.0.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9e2d922824181480953426608b81967de705c3cef4d1af983af849d7bd619158"}, - {file = "MarkupSafe-3.0.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:38a9ef736c01fccdd6600705b09dc574584b89bea478200c5fbf112a6b0d5579"}, - {file = "MarkupSafe-3.0.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bbcb445fa71794da8f178f0f6d66789a28d7319071af7a496d4d507ed566270d"}, - {file = "MarkupSafe-3.0.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:57cb5a3cf367aeb1d316576250f65edec5bb3be939e9247ae594b4bcbc317dfb"}, - {file = "MarkupSafe-3.0.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:3809ede931876f5b2ec92eef964286840ed3540dadf803dd570c3b7e13141a3b"}, - {file = "MarkupSafe-3.0.2-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:e07c3764494e3776c602c1e78e298937c3315ccc9043ead7e685b7f2b8d47b3c"}, - {file = "MarkupSafe-3.0.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:b424c77b206d63d500bcb69fa55ed8d0e6a3774056bdc4839fc9298a7edca171"}, - {file = "MarkupSafe-3.0.2-cp310-cp310-win32.whl", hash = "sha256:fcabf5ff6eea076f859677f5f0b6b5c1a51e70a376b0579e0eadef8db48c6b50"}, - {file = "MarkupSafe-3.0.2-cp310-cp310-win_amd64.whl", hash = "sha256:6af100e168aa82a50e186c82875a5893c5597a0c1ccdb0d8b40240b1f28b969a"}, - {file = "MarkupSafe-3.0.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:9025b4018f3a1314059769c7bf15441064b2207cb3f065e6ea1e7359cb46db9d"}, - {file = "MarkupSafe-3.0.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:93335ca3812df2f366e80509ae119189886b0f3c2b81325d39efdb84a1e2ae93"}, - {file = "MarkupSafe-3.0.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2cb8438c3cbb25e220c2ab33bb226559e7afb3baec11c4f218ffa7308603c832"}, - {file = "MarkupSafe-3.0.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a123e330ef0853c6e822384873bef7507557d8e4a082961e1defa947aa59ba84"}, - {file = "MarkupSafe-3.0.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1e084f686b92e5b83186b07e8a17fc09e38fff551f3602b249881fec658d3eca"}, - {file = "MarkupSafe-3.0.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d8213e09c917a951de9d09ecee036d5c7d36cb6cb7dbaece4c71a60d79fb9798"}, - {file = "MarkupSafe-3.0.2-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:5b02fb34468b6aaa40dfc198d813a641e3a63b98c2b05a16b9f80b7ec314185e"}, - {file = "MarkupSafe-3.0.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:0bff5e0ae4ef2e1ae4fdf2dfd5b76c75e5c2fa4132d05fc1b0dabcd20c7e28c4"}, - {file = "MarkupSafe-3.0.2-cp311-cp311-win32.whl", hash = "sha256:6c89876f41da747c8d3677a2b540fb32ef5715f97b66eeb0c6b66f5e3ef6f59d"}, - {file = "MarkupSafe-3.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:70a87b411535ccad5ef2f1df5136506a10775d267e197e4cf531ced10537bd6b"}, - {file = "MarkupSafe-3.0.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:9778bd8ab0a994ebf6f84c2b949e65736d5575320a17ae8984a77fab08db94cf"}, - {file = "MarkupSafe-3.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:846ade7b71e3536c4e56b386c2a47adf5741d2d8b94ec9dc3e92e5e1ee1e2225"}, - {file = "MarkupSafe-3.0.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1c99d261bd2d5f6b59325c92c73df481e05e57f19837bdca8413b9eac4bd8028"}, - {file = "MarkupSafe-3.0.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e17c96c14e19278594aa4841ec148115f9c7615a47382ecb6b82bd8fea3ab0c8"}, - {file = "MarkupSafe-3.0.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:88416bd1e65dcea10bc7569faacb2c20ce071dd1f87539ca2ab364bf6231393c"}, - {file = "MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:2181e67807fc2fa785d0592dc2d6206c019b9502410671cc905d132a92866557"}, - {file = "MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:52305740fe773d09cffb16f8ed0427942901f00adedac82ec8b67752f58a1b22"}, - {file = "MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ad10d3ded218f1039f11a75f8091880239651b52e9bb592ca27de44eed242a48"}, - {file = "MarkupSafe-3.0.2-cp312-cp312-win32.whl", hash = "sha256:0f4ca02bea9a23221c0182836703cbf8930c5e9454bacce27e767509fa286a30"}, - {file = "MarkupSafe-3.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:8e06879fc22a25ca47312fbe7c8264eb0b662f6db27cb2d3bbbc74b1df4b9b87"}, - {file = "MarkupSafe-3.0.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ba9527cdd4c926ed0760bc301f6728ef34d841f405abf9d4f959c478421e4efd"}, - {file = "MarkupSafe-3.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f8b3d067f2e40fe93e1ccdd6b2e1d16c43140e76f02fb1319a05cf2b79d99430"}, - {file = "MarkupSafe-3.0.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:569511d3b58c8791ab4c2e1285575265991e6d8f8700c7be0e88f86cb0672094"}, - {file = "MarkupSafe-3.0.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:15ab75ef81add55874e7ab7055e9c397312385bd9ced94920f2802310c930396"}, - {file = "MarkupSafe-3.0.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f3818cb119498c0678015754eba762e0d61e5b52d34c8b13d770f0719f7b1d79"}, - {file = "MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:cdb82a876c47801bb54a690c5ae105a46b392ac6099881cdfb9f6e95e4014c6a"}, - {file = "MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:cabc348d87e913db6ab4aa100f01b08f481097838bdddf7c7a84b7575b7309ca"}, - {file = "MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:444dcda765c8a838eaae23112db52f1efaf750daddb2d9ca300bcae1039adc5c"}, - {file = "MarkupSafe-3.0.2-cp313-cp313-win32.whl", hash = "sha256:bcf3e58998965654fdaff38e58584d8937aa3096ab5354d493c77d1fdd66d7a1"}, - {file = "MarkupSafe-3.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:e6a2a455bd412959b57a172ce6328d2dd1f01cb2135efda2e4576e8a23fa3b0f"}, - {file = "MarkupSafe-3.0.2-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:b5a6b3ada725cea8a5e634536b1b01c30bcdcd7f9c6fff4151548d5bf6b3a36c"}, - {file = "MarkupSafe-3.0.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:a904af0a6162c73e3edcb969eeeb53a63ceeb5d8cf642fade7d39e7963a22ddb"}, - {file = "MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4aa4e5faecf353ed117801a068ebab7b7e09ffb6e1d5e412dc852e0da018126c"}, - {file = "MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c0ef13eaeee5b615fb07c9a7dadb38eac06a0608b41570d8ade51c56539e509d"}, - {file = "MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d16a81a06776313e817c951135cf7340a3e91e8c1ff2fac444cfd75fffa04afe"}, - {file = "MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:6381026f158fdb7c72a168278597a5e3a5222e83ea18f543112b2662a9b699c5"}, - {file = "MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:3d79d162e7be8f996986c064d1c7c817f6df3a77fe3d6859f6f9e7be4b8c213a"}, - {file = "MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:131a3c7689c85f5ad20f9f6fb1b866f402c445b220c19fe4308c0b147ccd2ad9"}, - {file = "MarkupSafe-3.0.2-cp313-cp313t-win32.whl", hash = "sha256:ba8062ed2cf21c07a9e295d5b8a2a5ce678b913b45fdf68c32d95d6c1291e0b6"}, - {file = "MarkupSafe-3.0.2-cp313-cp313t-win_amd64.whl", hash = "sha256:e444a31f8db13eb18ada366ab3cf45fd4b31e4db1236a4448f68778c1d1a5a2f"}, - {file = "MarkupSafe-3.0.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:eaa0a10b7f72326f1372a713e73c3f739b524b3af41feb43e4921cb529f5929a"}, - {file = "MarkupSafe-3.0.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:48032821bbdf20f5799ff537c7ac3d1fba0ba032cfc06194faffa8cda8b560ff"}, - {file = "MarkupSafe-3.0.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1a9d3f5f0901fdec14d8d2f66ef7d035f2157240a433441719ac9a3fba440b13"}, - {file = "MarkupSafe-3.0.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:88b49a3b9ff31e19998750c38e030fc7bb937398b1f78cfa599aaef92d693144"}, - {file = "MarkupSafe-3.0.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cfad01eed2c2e0c01fd0ecd2ef42c492f7f93902e39a42fc9ee1692961443a29"}, - {file = "MarkupSafe-3.0.2-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:1225beacc926f536dc82e45f8a4d68502949dc67eea90eab715dea3a21c1b5f0"}, - {file = "MarkupSafe-3.0.2-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:3169b1eefae027567d1ce6ee7cae382c57fe26e82775f460f0b2778beaad66c0"}, - {file = "MarkupSafe-3.0.2-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:eb7972a85c54febfb25b5c4b4f3af4dcc731994c7da0d8a0b4a6eb0640e1d178"}, - {file = "MarkupSafe-3.0.2-cp39-cp39-win32.whl", hash = "sha256:8c4e8c3ce11e1f92f6536ff07154f9d49677ebaaafc32db9db4620bc11ed480f"}, - {file = "MarkupSafe-3.0.2-cp39-cp39-win_amd64.whl", hash = "sha256:6e296a513ca3d94054c2c881cc913116e90fd030ad1c656b3869762b754f5f8a"}, - {file = "markupsafe-3.0.2.tar.gz", hash = "sha256:ee55d3edf80167e48ea11a923c7386f4669df67d7994554387f84e7d8b0a2bf0"}, + {file = "markupsafe-3.0.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:2f981d352f04553a7171b8e44369f2af4055f888dfb147d55e42d29e29e74559"}, + {file = "markupsafe-3.0.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e1c1493fb6e50ab01d20a22826e57520f1284df32f2d8601fdd90b6304601419"}, + {file = "markupsafe-3.0.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1ba88449deb3de88bd40044603fafffb7bc2b055d626a330323a9ed736661695"}, + {file = "markupsafe-3.0.3-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f42d0984e947b8adf7dd6dde396e720934d12c506ce84eea8476409563607591"}, + {file = "markupsafe-3.0.3-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c0c0b3ade1c0b13b936d7970b1d37a57acde9199dc2aecc4c336773e1d86049c"}, + {file = "markupsafe-3.0.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:0303439a41979d9e74d18ff5e2dd8c43ed6c6001fd40e5bf2e43f7bd9bbc523f"}, + {file = "markupsafe-3.0.3-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:d2ee202e79d8ed691ceebae8e0486bd9a2cd4794cec4824e1c99b6f5009502f6"}, + {file = "markupsafe-3.0.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:177b5253b2834fe3678cb4a5f0059808258584c559193998be2601324fdeafb1"}, + {file = "markupsafe-3.0.3-cp310-cp310-win32.whl", hash = "sha256:2a15a08b17dd94c53a1da0438822d70ebcd13f8c3a95abe3a9ef9f11a94830aa"}, + {file = "markupsafe-3.0.3-cp310-cp310-win_amd64.whl", hash = "sha256:c4ffb7ebf07cfe8931028e3e4c85f0357459a3f9f9490886198848f4fa002ec8"}, + {file = "markupsafe-3.0.3-cp310-cp310-win_arm64.whl", hash = "sha256:e2103a929dfa2fcaf9bb4e7c091983a49c9ac3b19c9061b6d5427dd7d14d81a1"}, + {file = "markupsafe-3.0.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:1cc7ea17a6824959616c525620e387f6dd30fec8cb44f649e31712db02123dad"}, + {file = "markupsafe-3.0.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4bd4cd07944443f5a265608cc6aab442e4f74dff8088b0dfc8238647b8f6ae9a"}, + {file = "markupsafe-3.0.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6b5420a1d9450023228968e7e6a9ce57f65d148ab56d2313fcd589eee96a7a50"}, + {file = "markupsafe-3.0.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0bf2a864d67e76e5c9a34dc26ec616a66b9888e25e7b9460e1c76d3293bd9dbf"}, + {file = "markupsafe-3.0.3-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:bc51efed119bc9cfdf792cdeaa4d67e8f6fcccab66ed4bfdd6bde3e59bfcbb2f"}, + {file = "markupsafe-3.0.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:068f375c472b3e7acbe2d5318dea141359e6900156b5b2ba06a30b169086b91a"}, + {file = "markupsafe-3.0.3-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:7be7b61bb172e1ed687f1754f8e7484f1c8019780f6f6b0786e76bb01c2ae115"}, + {file = "markupsafe-3.0.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f9e130248f4462aaa8e2552d547f36ddadbeaa573879158d721bbd33dfe4743a"}, + {file = "markupsafe-3.0.3-cp311-cp311-win32.whl", hash = "sha256:0db14f5dafddbb6d9208827849fad01f1a2609380add406671a26386cdf15a19"}, + {file = "markupsafe-3.0.3-cp311-cp311-win_amd64.whl", hash = "sha256:de8a88e63464af587c950061a5e6a67d3632e36df62b986892331d4620a35c01"}, + {file = "markupsafe-3.0.3-cp311-cp311-win_arm64.whl", hash = "sha256:3b562dd9e9ea93f13d53989d23a7e775fdfd1066c33494ff43f5418bc8c58a5c"}, + {file = "markupsafe-3.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:d53197da72cc091b024dd97249dfc7794d6a56530370992a5e1a08983ad9230e"}, + {file = "markupsafe-3.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1872df69a4de6aead3491198eaf13810b565bdbeec3ae2dc8780f14458ec73ce"}, + {file = "markupsafe-3.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3a7e8ae81ae39e62a41ec302f972ba6ae23a5c5396c8e60113e9066ef893da0d"}, + {file = "markupsafe-3.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d6dd0be5b5b189d31db7cda48b91d7e0a9795f31430b7f271219ab30f1d3ac9d"}, + {file = "markupsafe-3.0.3-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:94c6f0bb423f739146aec64595853541634bde58b2135f27f61c1ffd1cd4d16a"}, + {file = "markupsafe-3.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:be8813b57049a7dc738189df53d69395eba14fb99345e0a5994914a3864c8a4b"}, + {file = "markupsafe-3.0.3-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:83891d0e9fb81a825d9a6d61e3f07550ca70a076484292a70fde82c4b807286f"}, + {file = "markupsafe-3.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:77f0643abe7495da77fb436f50f8dab76dbc6e5fd25d39589a0f1fe6548bfa2b"}, + {file = "markupsafe-3.0.3-cp312-cp312-win32.whl", hash = "sha256:d88b440e37a16e651bda4c7c2b930eb586fd15ca7406cb39e211fcff3bf3017d"}, + {file = "markupsafe-3.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:26a5784ded40c9e318cfc2bdb30fe164bdb8665ded9cd64d500a34fb42067b1c"}, + {file = "markupsafe-3.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:35add3b638a5d900e807944a078b51922212fb3dedb01633a8defc4b01a3c85f"}, + {file = "markupsafe-3.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e1cf1972137e83c5d4c136c43ced9ac51d0e124706ee1c8aa8532c1287fa8795"}, + {file = "markupsafe-3.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:116bb52f642a37c115f517494ea5feb03889e04df47eeff5b130b1808ce7c219"}, + {file = "markupsafe-3.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:133a43e73a802c5562be9bbcd03d090aa5a1fe899db609c29e8c8d815c5f6de6"}, + {file = "markupsafe-3.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ccfcd093f13f0f0b7fdd0f198b90053bf7b2f02a3927a30e63f3ccc9df56b676"}, + {file = "markupsafe-3.0.3-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:509fa21c6deb7a7a273d629cf5ec029bc209d1a51178615ddf718f5918992ab9"}, + {file = "markupsafe-3.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a4afe79fb3de0b7097d81da19090f4df4f8d3a2b3adaa8764138aac2e44f3af1"}, + {file = "markupsafe-3.0.3-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:795e7751525cae078558e679d646ae45574b47ed6e7771863fcc079a6171a0fc"}, + {file = "markupsafe-3.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8485f406a96febb5140bfeca44a73e3ce5116b2501ac54fe953e488fb1d03b12"}, + {file = "markupsafe-3.0.3-cp313-cp313-win32.whl", hash = "sha256:bdd37121970bfd8be76c5fb069c7751683bdf373db1ed6c010162b2a130248ed"}, + {file = "markupsafe-3.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:9a1abfdc021a164803f4d485104931fb8f8c1efd55bc6b748d2f5774e78b62c5"}, + {file = "markupsafe-3.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:7e68f88e5b8799aa49c85cd116c932a1ac15caaa3f5db09087854d218359e485"}, + {file = "markupsafe-3.0.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:218551f6df4868a8d527e3062d0fb968682fe92054e89978594c28e642c43a73"}, + {file = "markupsafe-3.0.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:3524b778fe5cfb3452a09d31e7b5adefeea8c5be1d43c4f810ba09f2ceb29d37"}, + {file = "markupsafe-3.0.3-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4e885a3d1efa2eadc93c894a21770e4bc67899e3543680313b09f139e149ab19"}, + {file = "markupsafe-3.0.3-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8709b08f4a89aa7586de0aadc8da56180242ee0ada3999749b183aa23df95025"}, + {file = "markupsafe-3.0.3-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:b8512a91625c9b3da6f127803b166b629725e68af71f8184ae7e7d54686a56d6"}, + {file = "markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:9b79b7a16f7fedff2495d684f2b59b0457c3b493778c9eed31111be64d58279f"}, + {file = "markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:12c63dfb4a98206f045aa9563db46507995f7ef6d83b2f68eda65c307c6829eb"}, + {file = "markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:8f71bc33915be5186016f675cd83a1e08523649b0e33efdb898db577ef5bb009"}, + {file = "markupsafe-3.0.3-cp313-cp313t-win32.whl", hash = "sha256:69c0b73548bc525c8cb9a251cddf1931d1db4d2258e9599c28c07ef3580ef354"}, + {file = "markupsafe-3.0.3-cp313-cp313t-win_amd64.whl", hash = "sha256:1b4b79e8ebf6b55351f0d91fe80f893b4743f104bff22e90697db1590e47a218"}, + {file = "markupsafe-3.0.3-cp313-cp313t-win_arm64.whl", hash = "sha256:ad2cf8aa28b8c020ab2fc8287b0f823d0a7d8630784c31e9ee5edea20f406287"}, + {file = "markupsafe-3.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:eaa9599de571d72e2daf60164784109f19978b327a3910d3e9de8c97b5b70cfe"}, + {file = "markupsafe-3.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c47a551199eb8eb2121d4f0f15ae0f923d31350ab9280078d1e5f12b249e0026"}, + {file = "markupsafe-3.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f34c41761022dd093b4b6896d4810782ffbabe30f2d443ff5f083e0cbbb8c737"}, + {file = "markupsafe-3.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:457a69a9577064c05a97c41f4e65148652db078a3a509039e64d3467b9e7ef97"}, + {file = "markupsafe-3.0.3-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e8afc3f2ccfa24215f8cb28dcf43f0113ac3c37c2f0f0806d8c70e4228c5cf4d"}, + {file = "markupsafe-3.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:ec15a59cf5af7be74194f7ab02d0f59a62bdcf1a537677ce67a2537c9b87fcda"}, + {file = "markupsafe-3.0.3-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:0eb9ff8191e8498cca014656ae6b8d61f39da5f95b488805da4bb029cccbfbaf"}, + {file = "markupsafe-3.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:2713baf880df847f2bece4230d4d094280f4e67b1e813eec43b4c0e144a34ffe"}, + {file = "markupsafe-3.0.3-cp314-cp314-win32.whl", hash = "sha256:729586769a26dbceff69f7a7dbbf59ab6572b99d94576a5592625d5b411576b9"}, + {file = "markupsafe-3.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:bdc919ead48f234740ad807933cdf545180bfbe9342c2bb451556db2ed958581"}, + {file = "markupsafe-3.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:5a7d5dc5140555cf21a6fefbdbf8723f06fcd2f63ef108f2854de715e4422cb4"}, + {file = "markupsafe-3.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:1353ef0c1b138e1907ae78e2f6c63ff67501122006b0f9abad68fda5f4ffc6ab"}, + {file = "markupsafe-3.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:1085e7fbddd3be5f89cc898938f42c0b3c711fdcb37d75221de2666af647c175"}, + {file = "markupsafe-3.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1b52b4fb9df4eb9ae465f8d0c228a00624de2334f216f178a995ccdcf82c4634"}, + {file = "markupsafe-3.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fed51ac40f757d41b7c48425901843666a6677e3e8eb0abcff09e4ba6e664f50"}, + {file = "markupsafe-3.0.3-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f190daf01f13c72eac4efd5c430a8de82489d9cff23c364c3ea822545032993e"}, + {file = "markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e56b7d45a839a697b5eb268c82a71bd8c7f6c94d6fd50c3d577fa39a9f1409f5"}, + {file = "markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:f3e98bb3798ead92273dc0e5fd0f31ade220f59a266ffd8a4f6065e0a3ce0523"}, + {file = "markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:5678211cb9333a6468fb8d8be0305520aa073f50d17f089b5b4b477ea6e67fdc"}, + {file = "markupsafe-3.0.3-cp314-cp314t-win32.whl", hash = "sha256:915c04ba3851909ce68ccc2b8e2cd691618c4dc4c4232fb7982bca3f41fd8c3d"}, + {file = "markupsafe-3.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4faffd047e07c38848ce017e8725090413cd80cbc23d86e55c587bf979e579c9"}, + {file = "markupsafe-3.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:32001d6a8fc98c8cb5c947787c5d08b0a50663d139f1305bac5885d98d9b40fa"}, + {file = "markupsafe-3.0.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:15d939a21d546304880945ca1ecb8a039db6b4dc49b2c5a400387cdae6a62e26"}, + {file = "markupsafe-3.0.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:f71a396b3bf33ecaa1626c255855702aca4d3d9fea5e051b41ac59a9c1c41edc"}, + {file = "markupsafe-3.0.3-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0f4b68347f8c5eab4a13419215bdfd7f8c9b19f2b25520968adfad23eb0ce60c"}, + {file = "markupsafe-3.0.3-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e8fc20152abba6b83724d7ff268c249fa196d8259ff481f3b1476383f8f24e42"}, + {file = "markupsafe-3.0.3-cp39-cp39-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:949b8d66bc381ee8b007cd945914c721d9aba8e27f71959d750a46f7c282b20b"}, + {file = "markupsafe-3.0.3-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:3537e01efc9d4dccdf77221fb1cb3b8e1a38d5428920e0657ce299b20324d758"}, + {file = "markupsafe-3.0.3-cp39-cp39-musllinux_1_2_riscv64.whl", hash = "sha256:591ae9f2a647529ca990bc681daebdd52c8791ff06c2bfa05b65163e28102ef2"}, + {file = "markupsafe-3.0.3-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:a320721ab5a1aba0a233739394eb907f8c8da5c98c9181d1161e77a0c8e36f2d"}, + {file = "markupsafe-3.0.3-cp39-cp39-win32.whl", hash = "sha256:df2449253ef108a379b8b5d6b43f4b1a8e81a061d6537becd5582fba5f9196d7"}, + {file = "markupsafe-3.0.3-cp39-cp39-win_amd64.whl", hash = "sha256:7c3fb7d25180895632e5d3148dbdc29ea38ccb7fd210aa27acbd1201a1902c6e"}, + {file = "markupsafe-3.0.3-cp39-cp39-win_arm64.whl", hash = "sha256:38664109c14ffc9e7437e86b4dceb442b0096dfe3541d7864d9cbe1da4cf36c8"}, + {file = "markupsafe-3.0.3.tar.gz", hash = "sha256:722695808f4b6457b320fdc131280796bdceb04ab50fe1795cd540799ebe1698"}, ] [[package]] @@ -1400,7 +1553,7 @@ description = "An LDAP3 auth provider for Synapse" optional = true python-versions = ">=3.7" groups = ["main"] -markers = "extra == \"all\" or extra == \"matrix-synapse-ldap3\"" +markers = "extra == \"matrix-synapse-ldap3\" or extra == \"all\"" files = [ {file = "matrix-synapse-ldap3-0.3.0.tar.gz", hash = "sha256:8bb6517173164d4b9cc44f49de411d8cebdb2e705d5dd1ea1f38733c4a009e1d"}, {file = "matrix_synapse_ldap3-0.3.0-py3-none-any.whl", hash = "sha256:8b4d701f8702551e98cc1d8c20dbed532de5613584c08d0df22de376ba99159d"}, @@ -1428,15 +1581,15 @@ files = [ [[package]] name = "more-itertools" -version = "9.1.0" +version = "10.8.0" description = "More routines for operating on iterables, beyond itertools" optional = false -python-versions = ">=3.7" +python-versions = ">=3.9" groups = ["dev"] markers = "platform_machine != \"ppc64le\" and platform_machine != \"s390x\"" files = [ - {file = "more-itertools-9.1.0.tar.gz", hash = "sha256:cabaa341ad0389ea83c17a94566a53ae4c9d07349861ecb14dc6d0345cf9ac5d"}, - {file = "more_itertools-9.1.0-py3-none-any.whl", hash = "sha256:d2bc7f02446e86a68911e58ded76d6561eea00cddfb2a91e7019bbb586c799f3"}, + {file = "more_itertools-10.8.0-py3-none-any.whl", hash = "sha256:52d4362373dcf7c52546bc4af9a86ee7c4579df9a8dc268be0a2f949d376cc9b"}, + {file = "more_itertools-10.8.0.tar.gz", hash = "sha256:f638ddf8a1a0d134181275fb5d58b086ead7c6a72429ad725c67503f13ba30bd"}, ] [[package]] @@ -1513,14 +1666,14 @@ files = [ [[package]] name = "multipart" -version = "1.3.0" +version = "1.3.1" description = "Parser for multipart/form-data" optional = false python-versions = ">=3.8" groups = ["main"] files = [ - {file = "multipart-1.3.0-py3-none-any.whl", hash = "sha256:439bf4b00fd7cb2dbff08ae13f49f4f49798931ecd8d496372c63537fa19f304"}, - {file = "multipart-1.3.0.tar.gz", hash = "sha256:a46bd6b0eb4c1ba865beb88ddd886012a3da709b6e7b86084fc37e99087e5cf1"}, + {file = "multipart-1.3.1-py3-none-any.whl", hash = "sha256:a82b59e1befe74d3d30b3d3f70efd5a2eba4d938f845dcff9faace968888ff29"}, + {file = "multipart-1.3.1.tar.gz", hash = "sha256:211d7cfc1a7a43e75c4d24ee0e8e0f4f61d522f1a21575303ae85333dea687bf"}, ] [package.extras] @@ -1529,54 +1682,54 @@ docs = ["sphinx (>=8,<9)", "sphinx-autobuild"] [[package]] name = "mypy" -version = "1.19.0" +version = "1.19.1" description = "Optional static typing for Python" optional = false python-versions = ">=3.9" groups = ["dev"] files = [ - {file = "mypy-1.19.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:6148ede033982a8c5ca1143de34c71836a09f105068aaa8b7d5edab2b053e6c8"}, - {file = "mypy-1.19.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a9ac09e52bb0f7fb912f5d2a783345c72441a08ef56ce3e17c1752af36340a39"}, - {file = "mypy-1.19.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:11f7254c15ab3f8ed68f8e8f5cbe88757848df793e31c36aaa4d4f9783fd08ab"}, - {file = "mypy-1.19.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:318ba74f75899b0e78b847d8c50821e4c9637c79d9a59680fc1259f29338cb3e"}, - {file = "mypy-1.19.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:cf7d84f497f78b682edd407f14a7b6e1a2212b433eedb054e2081380b7395aa3"}, - {file = "mypy-1.19.0-cp310-cp310-win_amd64.whl", hash = "sha256:c3385246593ac2b97f155a0e9639be906e73534630f663747c71908dfbf26134"}, - {file = "mypy-1.19.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:a31e4c28e8ddb042c84c5e977e28a21195d086aaffaf08b016b78e19c9ef8106"}, - {file = "mypy-1.19.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:34ec1ac66d31644f194b7c163d7f8b8434f1b49719d403a5d26c87fff7e913f7"}, - {file = "mypy-1.19.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:cb64b0ba5980466a0f3f9990d1c582bcab8db12e29815ecb57f1408d99b4bff7"}, - {file = "mypy-1.19.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:120cffe120cca5c23c03c77f84abc0c14c5d2e03736f6c312480020082f1994b"}, - {file = "mypy-1.19.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:7a500ab5c444268a70565e374fc803972bfd1f09545b13418a5174e29883dab7"}, - {file = "mypy-1.19.0-cp311-cp311-win_amd64.whl", hash = "sha256:c14a98bc63fd867530e8ec82f217dae29d0550c86e70debc9667fff1ec83284e"}, - {file = "mypy-1.19.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:0fb3115cb8fa7c5f887c8a8d81ccdcb94cff334684980d847e5a62e926910e1d"}, - {file = "mypy-1.19.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f3e19e3b897562276bb331074d64c076dbdd3e79213f36eed4e592272dabd760"}, - {file = "mypy-1.19.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b9d491295825182fba01b6ffe2c6fe4e5a49dbf4e2bb4d1217b6ced3b4797bc6"}, - {file = "mypy-1.19.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6016c52ab209919b46169651b362068f632efcd5eb8ef9d1735f6f86da7853b2"}, - {file = "mypy-1.19.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:f188dcf16483b3e59f9278c4ed939ec0254aa8a60e8fc100648d9ab5ee95a431"}, - {file = "mypy-1.19.0-cp312-cp312-win_amd64.whl", hash = "sha256:0e3c3d1e1d62e678c339e7ade72746a9e0325de42cd2cccc51616c7b2ed1a018"}, - {file = "mypy-1.19.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:7686ed65dbabd24d20066f3115018d2dce030d8fa9db01aa9f0a59b6813e9f9e"}, - {file = "mypy-1.19.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:fd4a985b2e32f23bead72e2fb4bbe5d6aceee176be471243bd831d5b2644672d"}, - {file = "mypy-1.19.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:fc51a5b864f73a3a182584b1ac75c404396a17eced54341629d8bdcb644a5bba"}, - {file = "mypy-1.19.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:37af5166f9475872034b56c5efdcf65ee25394e9e1d172907b84577120714364"}, - {file = "mypy-1.19.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:510c014b722308c9bd377993bcbf9a07d7e0692e5fa8fc70e639c1eb19fc6bee"}, - {file = "mypy-1.19.0-cp313-cp313-win_amd64.whl", hash = "sha256:cabbee74f29aa9cd3b444ec2f1e4fa5a9d0d746ce7567a6a609e224429781f53"}, - {file = "mypy-1.19.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:f2e36bed3c6d9b5f35d28b63ca4b727cb0228e480826ffc8953d1892ddc8999d"}, - {file = "mypy-1.19.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:a18d8abdda14035c5718acb748faec09571432811af129bf0d9e7b2d6699bf18"}, - {file = "mypy-1.19.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f75e60aca3723a23511948539b0d7ed514dda194bc3755eae0bfc7a6b4887aa7"}, - {file = "mypy-1.19.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8f44f2ae3c58421ee05fe609160343c25f70e3967f6e32792b5a78006a9d850f"}, - {file = "mypy-1.19.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:63ea6a00e4bd6822adbfc75b02ab3653a17c02c4347f5bb0cf1d5b9df3a05835"}, - {file = "mypy-1.19.0-cp314-cp314-win_amd64.whl", hash = "sha256:3ad925b14a0bb99821ff6f734553294aa6a3440a8cb082fe1f5b84dfb662afb1"}, - {file = "mypy-1.19.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:0dde5cb375cb94deff0d4b548b993bec52859d1651e073d63a1386d392a95495"}, - {file = "mypy-1.19.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:1cf9c59398db1c68a134b0b5354a09a1e124523f00bacd68e553b8bd16ff3299"}, - {file = "mypy-1.19.0-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3210d87b30e6af9c8faed61be2642fcbe60ef77cec64fa1ef810a630a4cf671c"}, - {file = "mypy-1.19.0-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e2c1101ab41d01303103ab6ef82cbbfedb81c1a060c868fa7cc013d573d37ab5"}, - {file = "mypy-1.19.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:0ea4fd21bb48f0da49e6d3b37ef6bd7e8228b9fe41bbf4d80d9364d11adbd43c"}, - {file = "mypy-1.19.0-cp39-cp39-win_amd64.whl", hash = "sha256:16f76ff3f3fd8137aadf593cb4607d82634fca675e8211ad75c43d86033ee6c6"}, - {file = "mypy-1.19.0-py3-none-any.whl", hash = "sha256:0c01c99d626380752e527d5ce8e69ffbba2046eb8a060db0329690849cf9b6f9"}, - {file = "mypy-1.19.0.tar.gz", hash = "sha256:f6b874ca77f733222641e5c46e4711648c4037ea13646fd0cdc814c2eaec2528"}, + {file = "mypy-1.19.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:5f05aa3d375b385734388e844bc01733bd33c644ab48e9684faa54e5389775ec"}, + {file = "mypy-1.19.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:022ea7279374af1a5d78dfcab853fe6a536eebfda4b59deab53cd21f6cd9f00b"}, + {file = "mypy-1.19.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ee4c11e460685c3e0c64a4c5de82ae143622410950d6be863303a1c4ba0e36d6"}, + {file = "mypy-1.19.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:de759aafbae8763283b2ee5869c7255391fbc4de3ff171f8f030b5ec48381b74"}, + {file = "mypy-1.19.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:ab43590f9cd5108f41aacf9fca31841142c786827a74ab7cc8a2eacb634e09a1"}, + {file = "mypy-1.19.1-cp310-cp310-win_amd64.whl", hash = "sha256:2899753e2f61e571b3971747e302d5f420c3fd09650e1951e99f823bc3089dac"}, + {file = "mypy-1.19.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:d8dfc6ab58ca7dda47d9237349157500468e404b17213d44fc1cb77bce532288"}, + {file = "mypy-1.19.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e3f276d8493c3c97930e354b2595a44a21348b320d859fb4a2b9f66da9ed27ab"}, + {file = "mypy-1.19.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2abb24cf3f17864770d18d673c85235ba52456b36a06b6afc1e07c1fdcd3d0e6"}, + {file = "mypy-1.19.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a009ffa5a621762d0c926a078c2d639104becab69e79538a494bcccb62cc0331"}, + {file = "mypy-1.19.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f7cee03c9a2e2ee26ec07479f38ea9c884e301d42c6d43a19d20fb014e3ba925"}, + {file = "mypy-1.19.1-cp311-cp311-win_amd64.whl", hash = "sha256:4b84a7a18f41e167f7995200a1d07a4a6810e89d29859df936f1c3923d263042"}, + {file = "mypy-1.19.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:a8174a03289288c1f6c46d55cef02379b478bfbc8e358e02047487cad44c6ca1"}, + {file = "mypy-1.19.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ffcebe56eb09ff0c0885e750036a095e23793ba6c2e894e7e63f6d89ad51f22e"}, + {file = "mypy-1.19.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b64d987153888790bcdb03a6473d321820597ab8dd9243b27a92153c4fa50fd2"}, + {file = "mypy-1.19.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c35d298c2c4bba75feb2195655dfea8124d855dfd7343bf8b8c055421eaf0cf8"}, + {file = "mypy-1.19.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:34c81968774648ab5ac09c29a375fdede03ba253f8f8287847bd480782f73a6a"}, + {file = "mypy-1.19.1-cp312-cp312-win_amd64.whl", hash = "sha256:b10e7c2cd7870ba4ad9b2d8a6102eb5ffc1f16ca35e3de6bfa390c1113029d13"}, + {file = "mypy-1.19.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e3157c7594ff2ef1634ee058aafc56a82db665c9438fd41b390f3bde1ab12250"}, + {file = "mypy-1.19.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:bdb12f69bcc02700c2b47e070238f42cb87f18c0bc1fc4cdb4fb2bc5fd7a3b8b"}, + {file = "mypy-1.19.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f859fb09d9583a985be9a493d5cfc5515b56b08f7447759a0c5deaf68d80506e"}, + {file = "mypy-1.19.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c9a6538e0415310aad77cb94004ca6482330fece18036b5f360b62c45814c4ef"}, + {file = "mypy-1.19.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:da4869fc5e7f62a88f3fe0b5c919d1d9f7ea3cef92d3689de2823fd27e40aa75"}, + {file = "mypy-1.19.1-cp313-cp313-win_amd64.whl", hash = "sha256:016f2246209095e8eda7538944daa1d60e1e8134d98983b9fc1e92c1fc0cb8dd"}, + {file = "mypy-1.19.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:06e6170bd5836770e8104c8fdd58e5e725cfeb309f0a6c681a811f557e97eac1"}, + {file = "mypy-1.19.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:804bd67b8054a85447c8954215a906d6eff9cabeabe493fb6334b24f4bfff718"}, + {file = "mypy-1.19.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:21761006a7f497cb0d4de3d8ef4ca70532256688b0523eee02baf9eec895e27b"}, + {file = "mypy-1.19.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:28902ee51f12e0f19e1e16fbe2f8f06b6637f482c459dd393efddd0ec7f82045"}, + {file = "mypy-1.19.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:481daf36a4c443332e2ae9c137dfee878fcea781a2e3f895d54bd3002a900957"}, + {file = "mypy-1.19.1-cp314-cp314-win_amd64.whl", hash = "sha256:8bb5c6f6d043655e055be9b542aa5f3bdd30e4f3589163e85f93f3640060509f"}, + {file = "mypy-1.19.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:7bcfc336a03a1aaa26dfce9fff3e287a3ba99872a157561cbfcebe67c13308e3"}, + {file = "mypy-1.19.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:b7951a701c07ea584c4fe327834b92a30825514c868b1f69c30445093fdd9d5a"}, + {file = "mypy-1.19.1-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b13cfdd6c87fc3efb69ea4ec18ef79c74c3f98b4e5498ca9b85ab3b2c2329a67"}, + {file = "mypy-1.19.1-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4f28f99c824ecebcdaa2e55d82953e38ff60ee5ec938476796636b86afa3956e"}, + {file = "mypy-1.19.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:c608937067d2fc5a4dd1a5ce92fd9e1398691b8c5d012d66e1ddd430e9244376"}, + {file = "mypy-1.19.1-cp39-cp39-win_amd64.whl", hash = "sha256:409088884802d511ee52ca067707b90c883426bd95514e8cfda8281dc2effe24"}, + {file = "mypy-1.19.1-py3-none-any.whl", hash = "sha256:f1235f5ea01b7db5468d53ece6aaddf1ad0b88d9e7462b86ef96fe04995d7247"}, + {file = "mypy-1.19.1.tar.gz", hash = "sha256:19d88bb05303fe63f71dd2c6270daca27cb9401c4ca8255fe50d1d920e0eb9ba"}, ] [package.dependencies] -librt = ">=0.6.2" +librt = {version = ">=0.6.2", markers = "platform_python_implementation != \"PyPy\""} mypy_extensions = ">=1.0.0" pathspec = ">=0.9.0" tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""} @@ -1591,14 +1744,14 @@ reports = ["lxml"] [[package]] name = "mypy-extensions" -version = "1.0.0" +version = "1.1.0" description = "Type system extensions for programs checked with the mypy type checker." optional = false -python-versions = ">=3.5" +python-versions = ">=3.8" groups = ["dev"] files = [ - {file = "mypy_extensions-1.0.0-py3-none-any.whl", hash = "sha256:4392f6c0eb8a5668a69e23d168ffa70f0be9ccfd32b5cc2d26a34ae5b844552d"}, - {file = "mypy_extensions-1.0.0.tar.gz", hash = "sha256:75dbf8955dc00442a438fc4d0666508a9a97b6bd41aa2f0ffe9d2f2725af0782"}, + {file = "mypy_extensions-1.1.0-py3-none-any.whl", hash = "sha256:1be4cccdb0f2482337c4743e60421de3a356cd97508abadd57d47403e94f5505"}, + {file = "mypy_extensions-1.1.0.tar.gz", hash = "sha256:52e68efc3284861e772bbcd66823fde5ae21fd2fdb51c62a211403730b916558"}, ] [[package]] @@ -1636,6 +1789,43 @@ files = [ [package.extras] nicer-shell = ["ipython"] +[[package]] +name = "nh3" +version = "0.3.3" +description = "Python binding to Ammonia HTML sanitizer Rust crate" +optional = false +python-versions = ">=3.8" +groups = ["dev"] +files = [ + {file = "nh3-0.3.3-cp314-cp314t-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:21b058cd20d9f0919421a820a2843fdb5e1749c0bf57a6247ab8f4ba6723c9fc"}, + {file = "nh3-0.3.3-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f4400a73c2a62859e769f9d36d1b5a7a5c65c4179d1dddd2f6f3095b2db0cbfc"}, + {file = "nh3-0.3.3-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:1ef87f8e916321a88b45f2d597f29bd56e560ed4568a50f0f1305afab86b7189"}, + {file = "nh3-0.3.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:a446eae598987f49ee97ac2f18eafcce4e62e7574bd1eb23782e4702e54e217d"}, + {file = "nh3-0.3.3-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:0d5eb734a78ac364af1797fef718340a373f626a9ff6b4fb0b4badf7927e7b81"}, + {file = "nh3-0.3.3-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:92a958e6f6d0100e025a5686aafd67e3c98eac67495728f8bb64fbeb3e474493"}, + {file = "nh3-0.3.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:9ed40cf8449a59a03aa465114fedce1ff7ac52561688811d047917cc878b19ca"}, + {file = "nh3-0.3.3-cp314-cp314t-win32.whl", hash = "sha256:b50c3770299fb2a7c1113751501e8878d525d15160a4c05194d7fe62b758aad8"}, + {file = "nh3-0.3.3-cp314-cp314t-win_amd64.whl", hash = "sha256:21a63ccb18ddad3f784bb775955839b8b80e347e597726f01e43ca1abcc5c808"}, + {file = "nh3-0.3.3-cp314-cp314t-win_arm64.whl", hash = "sha256:f508ddd4e2433fdcb78c790fc2d24e3a349ba775e5fa904af89891321d4844a3"}, + {file = "nh3-0.3.3-cp38-abi3-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:e8ee96156f7dfc6e30ecda650e480c5ae0a7d38f0c6fafc3c1c655e2500421d9"}, + {file = "nh3-0.3.3-cp38-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:45fe0d6a607264910daec30360c8a3b5b1500fd832d21b2da608256287bcb92d"}, + {file = "nh3-0.3.3-cp38-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5bc1d4b30ba1ba896669d944b6003630592665974bd11a3dc2f661bde92798a7"}, + {file = "nh3-0.3.3-cp38-abi3-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:f433a2dd66545aad4a720ad1b2150edcdca75bfff6f4e6f378ade1ec138d5e77"}, + {file = "nh3-0.3.3-cp38-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:52e973cb742e95b9ae1b35822ce23992428750f4b46b619fe86eba4205255b30"}, + {file = "nh3-0.3.3-cp38-abi3-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4c730617bdc15d7092dcc0469dc2826b914c8f874996d105b4bc3842a41c1cd9"}, + {file = "nh3-0.3.3-cp38-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e98fa3dbfd54e25487e36ba500bc29bca3a4cab4ffba18cfb1a35a2d02624297"}, + {file = "nh3-0.3.3-cp38-abi3-manylinux_2_31_riscv64.whl", hash = "sha256:3a62b8ae7c235481715055222e54c682422d0495a5c73326807d4e44c5d14691"}, + {file = "nh3-0.3.3-cp38-abi3-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:fc305a2264868ec8fa16548296f803d8fd9c1fa66cd28b88b605b1bd06667c0b"}, + {file = "nh3-0.3.3-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:90126a834c18af03bfd6ff9a027bfa6bbf0e238527bc780a24de6bd7cc1041e2"}, + {file = "nh3-0.3.3-cp38-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:24769a428e9e971e4ccfb24628f83aaa7dc3c8b41b130c8ddc1835fa1c924489"}, + {file = "nh3-0.3.3-cp38-abi3-musllinux_1_2_i686.whl", hash = "sha256:b7a18ee057761e455d58b9d31445c3e4b2594cff4ddb84d2e331c011ef46f462"}, + {file = "nh3-0.3.3-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:5a4b2c1f3e6f3cbe7048e17f4fefad3f8d3e14cc0fd08fb8599e0d5653f6b181"}, + {file = "nh3-0.3.3-cp38-abi3-win32.whl", hash = "sha256:e974850b131fdffa75e7ad8e0d9c7a855b96227b093417fdf1bd61656e530f37"}, + {file = "nh3-0.3.3-cp38-abi3-win_amd64.whl", hash = "sha256:2efd17c0355d04d39e6d79122b42662277ac10a17ea48831d90b46e5ef7e4fc0"}, + {file = "nh3-0.3.3-cp38-abi3-win_arm64.whl", hash = "sha256:b838e619f483531483d26d889438e53a880510e832d2aafe73f93b7b1ac2bce2"}, + {file = "nh3-0.3.3.tar.gz", hash = "sha256:185ed41b88c910b9ca8edc89ca3b4be688a12cb9de129d84befa2f74a0039fee"}, +] + [[package]] name = "opentracing" version = "2.4.0" @@ -1643,7 +1833,7 @@ description = "OpenTracing API for Python. See documentation at http://opentraci optional = true python-versions = "*" groups = ["main"] -markers = "extra == \"all\" or extra == \"opentracing\"" +markers = "extra == \"opentracing\" or extra == \"all\"" files = [ {file = "opentracing-2.4.0.tar.gz", hash = "sha256:a173117e6ef580d55874734d1fa7ecb6f3655160b8b8974a2a1e98e5ec9c840d"}, ] @@ -1653,14 +1843,14 @@ tests = ["Sphinx", "doubles", "flake8", "flake8-quotes", "gevent", "mock", "pyte [[package]] name = "packaging" -version = "25.0" +version = "26.0" description = "Core utilities for Python packages" optional = false python-versions = ">=3.8" groups = ["main", "dev"] files = [ - {file = "packaging-25.0-py3-none-any.whl", hash = "sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484"}, - {file = "packaging-25.0.tar.gz", hash = "sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f"}, + {file = "packaging-26.0-py3-none-any.whl", hash = "sha256:b36f1fef9334a5588b4166f8bcd26a14e521f2b55e6b9de3aaa80d3ff7a37529"}, + {file = "packaging-26.0.tar.gz", hash = "sha256:00243ae351a257117b6a241061796684b084ed1c516a08c48a3f7e147a9d80b4"}, ] [[package]] @@ -1680,26 +1870,32 @@ dev = ["jinja2"] [[package]] name = "pathspec" -version = "0.12.1" +version = "1.0.4" description = "Utility library for gitignore style pattern matching of file paths." optional = false -python-versions = ">=3.8" +python-versions = ">=3.9" groups = ["dev"] files = [ - {file = "pathspec-0.12.1-py3-none-any.whl", hash = "sha256:a0d503e138a4c123b27490a4f7beda6a01c6f288df0e4a8b79c7eb0dc7b4cc08"}, - {file = "pathspec-0.12.1.tar.gz", hash = "sha256:a482d51503a1ab33b1c67a6c3813a26953dbdc71c31dacaef9a838c4e29f5712"}, + {file = "pathspec-1.0.4-py3-none-any.whl", hash = "sha256:fb6ae2fd4e7c921a165808a552060e722767cfa526f99ca5156ed2ce45a5c723"}, + {file = "pathspec-1.0.4.tar.gz", hash = "sha256:0210e2ae8a21a9137c0d470578cb0e595af87edaa6ebf12ff176f14a02e0e645"}, ] +[package.extras] +hyperscan = ["hyperscan (>=0.7)"] +optional = ["typing-extensions (>=4)"] +re2 = ["google-re2 (>=1.1)"] +tests = ["pytest (>=9)", "typing-extensions (>=4.15)"] + [[package]] name = "phonenumbers" -version = "9.0.19" +version = "9.0.25" description = "Python version of Google's common library for parsing, formatting, storing and validating international phone numbers." optional = false -python-versions = "*" +python-versions = ">=2.5" groups = ["main"] files = [ - {file = "phonenumbers-9.0.19-py2.py3-none-any.whl", hash = "sha256:004abdfe2010518c2383f148515664a742e8a5d5540e07c049735c139d7e8b09"}, - {file = "phonenumbers-9.0.19.tar.gz", hash = "sha256:e0674e31554362f4d95383558f7aefde738ef2e7bf96d28a10afd3e87d63a65c"}, + {file = "phonenumbers-9.0.25-py2.py3-none-any.whl", hash = "sha256:b1fd6c20d588f5bcd40af3899d727a9f536364211ec6eac554fcd75ca58992a3"}, + {file = "phonenumbers-9.0.25.tar.gz", hash = "sha256:a5f236fa384c6a77378d7836c8e486ade5f984ad2e8e6cc0dbe5124315cdc81b"}, ] [[package]] @@ -1813,17 +2009,19 @@ xmp = ["defusedxml"] [[package]] name = "prometheus-client" -version = "0.23.1" +version = "0.24.1" description = "Python client for the Prometheus monitoring system." optional = false python-versions = ">=3.9" groups = ["main"] files = [ - {file = "prometheus_client-0.23.1-py3-none-any.whl", hash = "sha256:dd1913e6e76b59cfe44e7a4b83e01afc9873c1bdfd2ed8739f1e76aeca115f99"}, - {file = "prometheus_client-0.23.1.tar.gz", hash = "sha256:6ae8f9081eaaaf153a2e959d2e6c4f4fb57b12ef76c8c7980202f1e57b48b2ce"}, + {file = "prometheus_client-0.24.1-py3-none-any.whl", hash = "sha256:150db128af71a5c2482b36e588fc8a6b95e498750da4b17065947c16070f4055"}, + {file = "prometheus_client-0.24.1.tar.gz", hash = "sha256:7e0ced7fbbd40f7b84962d5d2ab6f17ef88a72504dcf7c0b40737b43b2a461f9"}, ] [package.extras] +aiohttp = ["aiohttp"] +django = ["django"] twisted = ["twisted"] [[package]] @@ -1833,7 +2031,7 @@ description = "psycopg2 - Python-PostgreSQL Database Adapter" optional = true python-versions = ">=3.9" groups = ["main"] -markers = "extra == \"all\" or extra == \"postgres\"" +markers = "extra == \"postgres\" or extra == \"all\"" 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"}, @@ -1851,7 +2049,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 == \"all\" or extra == \"postgres\")" +markers = "platform_python_implementation == \"PyPy\" and (extra == \"postgres\" or extra == \"all\")" files = [ {file = "psycopg2cffi-2.9.0.tar.gz", hash = "sha256:7e272edcd837de3a1d12b62185eb85c45a19feda9e62fa1b120c54f9e8d35c52"}, ] @@ -1867,7 +2065,7 @@ description = "A Simple library to enable psycopg2 compatability" optional = true python-versions = "*" groups = ["main"] -markers = "platform_python_implementation == \"PyPy\" and (extra == \"all\" or extra == \"postgres\")" +markers = "platform_python_implementation == \"PyPy\" and (extra == \"postgres\" or extra == \"all\")" files = [ {file = "psycopg2cffi-compat-1.1.tar.gz", hash = "sha256:d25e921748475522b33d13420aad5c2831c743227dc1f1f2585e0fdb5c914e05"}, ] @@ -1904,15 +2102,15 @@ pyasn1 = ">=0.6.1,<0.7.0" [[package]] name = "pycparser" -version = "2.21" +version = "3.0" description = "C parser in Python" optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +python-versions = ">=3.10" groups = ["main", "dev"] markers = "implementation_name != \"PyPy\"" files = [ - {file = "pycparser-2.21-py2.py3-none-any.whl", hash = "sha256:8ee45429555515e1f6b185e78100aea234072576aa43ab53aefcae078162fca9"}, - {file = "pycparser-2.21.tar.gz", hash = "sha256:e644fdec12f7872f86c58ff790da456218b10f863970249516d60a5eaca77206"}, + {file = "pycparser-3.0-py3-none-any.whl", hash = "sha256:b727414169a36b7d524c1c3e31839a521725078d7b2ff038656844266160a992"}, + {file = "pycparser-3.0.tar.gz", hash = "sha256:600f49d217304a5902ac3c37e1281c9fe94e4d0489de643a9504c5cdfdfc6b29"}, ] [[package]] @@ -2092,29 +2290,29 @@ urllib3 = ">=1.26.0" [[package]] name = "pygments" -version = "2.15.1" +version = "2.19.2" description = "Pygments is a syntax highlighting package written in Python." optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" groups = ["dev"] files = [ - {file = "Pygments-2.15.1-py3-none-any.whl", hash = "sha256:db2db3deb4b4179f399a09054b023b6a586b76499d36965813c71aa8ed7b5fd1"}, - {file = "Pygments-2.15.1.tar.gz", hash = "sha256:8ace4d3c1dd481894b2005f560ead0f9f19ee64fe983366be1a21e171d12775c"}, + {file = "pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b"}, + {file = "pygments-2.19.2.tar.gz", hash = "sha256:636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887"}, ] [package.extras] -plugins = ["importlib-metadata ; python_version < \"3.8\""] +windows-terminal = ["colorama (>=0.4.6)"] [[package]] name = "pyjwt" -version = "2.6.0" +version = "2.11.0" description = "JSON Web Token implementation in Python" optional = false -python-versions = ">=3.7" +python-versions = ">=3.9" groups = ["dev"] files = [ - {file = "PyJWT-2.6.0-py3-none-any.whl", hash = "sha256:d83c3d892a77bbb74d3e1a2cfa90afaadb60945205d1095d9221f04466f64c14"}, - {file = "PyJWT-2.6.0.tar.gz", hash = "sha256:69285c7e31fc44f68a1feb309e948e0df53259d579295e6cfe2b1792329f05fd"}, + {file = "pyjwt-2.11.0-py3-none-any.whl", hash = "sha256:94a6bde30eb5c8e04fee991062b534071fd1439ef58d2adc9ccb823e7bcd0469"}, + {file = "pyjwt-2.11.0.tar.gz", hash = "sha256:35f95c1f0fbe5d5ba6e43f00271c275f7a1a4db1dab27bf708073b75318ea623"}, ] [package.dependencies] @@ -2122,9 +2320,9 @@ cryptography = {version = ">=3.4.0", optional = true, markers = "extra == \"cryp [package.extras] crypto = ["cryptography (>=3.4.0)"] -dev = ["coverage[toml] (==5.0.4)", "cryptography (>=3.4.0)", "pre-commit", "pytest (>=6.0.0,<7.0.0)", "sphinx (>=4.5.0,<5.0.0)", "sphinx-rtd-theme", "zope.interface"] -docs = ["sphinx (>=4.5.0,<5.0.0)", "sphinx-rtd-theme", "zope.interface"] -tests = ["coverage[toml] (==5.0.4)", "pytest (>=6.0.0,<7.0.0)"] +dev = ["coverage[toml] (==7.10.7)", "cryptography (>=3.4.0)", "pre-commit", "pytest (>=8.4.2,<9.0.0)", "sphinx", "sphinx-rtd-theme", "zope.interface"] +docs = ["sphinx", "sphinx-rtd-theme", "zope.interface"] +tests = ["coverage[toml] (==7.10.7)", "pytest (>=8.4.2,<9.0.0)"] [[package]] name = "pymacaroons" @@ -2149,7 +2347,7 @@ description = "A development tool to measure, monitor and analyze the memory beh optional = true python-versions = ">=3.6" groups = ["main"] -markers = "extra == \"all\" or extra == \"cache-memory\"" +markers = "extra == \"cache-memory\" or extra == \"all\"" files = [ {file = "Pympler-1.0.1-py3-none-any.whl", hash = "sha256:d260dda9ae781e1eab6ea15bacb84015849833ba5555f141d2d9b7b7473b307d"}, {file = "Pympler-1.0.1.tar.gz", hash = "sha256:993f1a3599ca3f4fcd7160c7545ad06310c9e12f70174ae7ae8d4e25f6c5d3fa"}, @@ -2219,14 +2417,14 @@ test = ["pretend", "pytest (>=3.0.1)", "pytest-rerunfailures"] [[package]] name = "pyparsing" -version = "3.2.5" +version = "3.3.2" description = "pyparsing - Classes and methods to define and execute parsing grammars" optional = false python-versions = ">=3.9" groups = ["main"] files = [ - {file = "pyparsing-3.2.5-py3-none-any.whl", hash = "sha256:e38a4f02064cf41fe6593d328d0512495ad1f3d8a91c4f73fc401b3079a59a5e"}, - {file = "pyparsing-3.2.5.tar.gz", hash = "sha256:2df8d5b7b2802ef88e8d016a2eb9c7aeaa923529cd251ed0fe4608275d4105b6"}, + {file = "pyparsing-3.3.2-py3-none-any.whl", hash = "sha256:850ba148bd908d7e2411587e247a1e4f0327839c40e2e5e6d05a007ecc69911d"}, + {file = "pyparsing-3.3.2.tar.gz", hash = "sha256:c777f4d763f140633dcb6d8a3eda953bf7a214dc4eff598413c070bcdc117cbc"}, ] [package.extras] @@ -2281,7 +2479,7 @@ description = "Python implementation of SAML Version 2 Standard" optional = true python-versions = ">=3.9,<4.0" groups = ["main"] -markers = "extra == \"all\" or extra == \"saml2\"" +markers = "extra == \"saml2\" or extra == \"all\"" files = [ {file = "pysaml2-7.5.0-py3-none-any.whl", hash = "sha256:bc6627cc344476a83c757f440a73fda1369f13b6fda1b4e16bca63ffbabb5318"}, {file = "pysaml2-7.5.0.tar.gz", hash = "sha256:f36871d4e5ee857c6b85532e942550d2cf90ea4ee943d75eb681044bbc4f54f7"}, @@ -2301,15 +2499,15 @@ s2repoze = ["paste", "repoze.who", "zope.interface"] [[package]] name = "python-dateutil" -version = "2.8.2" +version = "2.9.0.post0" description = "Extensions to the standard Python datetime module" optional = true python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7" groups = ["main"] -markers = "extra == \"all\" or extra == \"saml2\"" +markers = "extra == \"saml2\" or extra == \"all\"" files = [ - {file = "python-dateutil-2.8.2.tar.gz", hash = "sha256:0123cacc1627ae19ddf3c27a5de5bd67ee4586fbdd6440d9748f8abb483d3e86"}, - {file = "python_dateutil-2.8.2-py2.py3-none-any.whl", hash = "sha256:961d03dc3453ebbc59dbdea9e4e11c5651520a876d0f4db161e8674aae935da9"}, + {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"}, ] [package.dependencies] @@ -2329,28 +2527,28 @@ files = [ [[package]] name = "pytz" -version = "2025.2" +version = "2026.1.post1" description = "World timezone definitions, modern and historical" optional = true python-versions = "*" groups = ["main"] -markers = "extra == \"all\" or extra == \"saml2\"" +markers = "extra == \"saml2\" or extra == \"all\"" files = [ - {file = "pytz-2025.2-py2.py3-none-any.whl", hash = "sha256:5ddf76296dd8c44c26eb8f4b6f35488f3ccbf6fbbd7adee0b7262d43f0ec2f00"}, - {file = "pytz-2025.2.tar.gz", hash = "sha256:360b9e3dbb49a209c21ad61809c7fb453643e048b38924c765813546746e81c3"}, + {file = "pytz-2026.1.post1-py2.py3-none-any.whl", hash = "sha256:f2fd16142fda348286a75e1a524be810bb05d444e5a081f37f7affc635035f7a"}, + {file = "pytz-2026.1.post1.tar.gz", hash = "sha256:3378dde6a0c3d26719182142c56e60c7f9af7e968076f31aae569d72a0358ee1"}, ] [[package]] name = "pywin32-ctypes" -version = "0.2.0" -description = "" +version = "0.2.3" +description = "A (partial) reimplementation of pywin32 using ctypes/cffi" optional = false -python-versions = "*" +python-versions = ">=3.6" groups = ["dev"] markers = "platform_machine != \"ppc64le\" and platform_machine != \"s390x\" and sys_platform == \"win32\"" files = [ - {file = "pywin32-ctypes-0.2.0.tar.gz", hash = "sha256:24ffc3b341d457d48e8922352130cf2644024a4ff09762a2261fd34c36ee5942"}, - {file = "pywin32_ctypes-0.2.0-py2.py3-none-any.whl", hash = "sha256:9dc2d991b3479cc2df15930958b674a48a227d5361d413827a4cfd0b5876fc98"}, + {file = "pywin32-ctypes-0.2.3.tar.gz", hash = "sha256:d162dc04946d704503b2edc4d55f3dba5c1d539ead017afa00142c38b9885755"}, + {file = "pywin32_ctypes-0.2.3-py3-none-any.whl", hash = "sha256:8a1513379d709975552d202d942d9837758905c8d01eb82b8bcc30918929e7b8"}, ] [[package]] @@ -2438,19 +2636,19 @@ files = [ [[package]] name = "readme-renderer" -version = "37.3" -description = "readme_renderer is a library for rendering \"readme\" descriptions for Warehouse" +version = "44.0" +description = "readme_renderer is a library for rendering readme descriptions for Warehouse" optional = false -python-versions = ">=3.7" +python-versions = ">=3.9" groups = ["dev"] files = [ - {file = "readme_renderer-37.3-py3-none-any.whl", hash = "sha256:f67a16caedfa71eef48a31b39708637a6f4664c4394801a7b0d6432d13907343"}, - {file = "readme_renderer-37.3.tar.gz", hash = "sha256:cd653186dfc73055656f090f227f5cb22a046d7f71a841dfa305f55c9a513273"}, + {file = "readme_renderer-44.0-py3-none-any.whl", hash = "sha256:2fbca89b81a08526aadf1357a8c2ae889ec05fb03f5da67f9769c9a592166151"}, + {file = "readme_renderer-44.0.tar.gz", hash = "sha256:8712034eabbfa6805cacf1402b4eeb2a73028f72d1166d6f5cb7f9c047c5d1e1"}, ] [package.dependencies] -bleach = ">=2.1.0" -docutils = ">=0.13.1" +docutils = ">=0.21.2" +nh3 = ">=0.2.14" Pygments = ">=2.5.1" [package.extras] @@ -2458,30 +2656,31 @@ md = ["cmarkgfm (>=0.8.0)"] [[package]] name = "referencing" -version = "0.29.1" +version = "0.37.0" description = "JSON Referencing + Python" optional = false -python-versions = ">=3.8" +python-versions = ">=3.10" groups = ["main", "dev"] files = [ - {file = "referencing-0.29.1-py3-none-any.whl", hash = "sha256:d3c8f323ee1480095da44d55917cfb8278d73d6b4d5f677e3e40eb21314ac67f"}, - {file = "referencing-0.29.1.tar.gz", hash = "sha256:90cb53782d550ba28d2166ef3f55731f38397def8832baac5d45235f1995e35e"}, + {file = "referencing-0.37.0-py3-none-any.whl", hash = "sha256:381329a9f99628c9069361716891d34ad94af76e461dcb0335825aecc7692231"}, + {file = "referencing-0.37.0.tar.gz", hash = "sha256:44aefc3142c5b842538163acb373e24cce6632bd54bdb01b21ad5863489f50d8"}, ] [package.dependencies] attrs = ">=22.2.0" rpds-py = ">=0.7.0" +typing-extensions = {version = ">=4.4.0", markers = "python_version < \"3.13\""} [[package]] name = "requests" -version = "2.32.4" +version = "2.32.5" description = "Python HTTP for Humans." optional = false -python-versions = ">=3.8" +python-versions = ">=3.9" groups = ["main", "dev"] files = [ - {file = "requests-2.32.4-py3-none-any.whl", hash = "sha256:27babd3cda2a6d50b30443204ee89830707d396671944c998b5975b031ac2b2c"}, - {file = "requests-2.32.4.tar.gz", hash = "sha256:27d0316682c8a29834d3264820024b62a36942083d52caf2f14c0591336d3422"}, + {file = "requests-2.32.5-py3-none-any.whl", hash = "sha256:2462f94637a34fd532264295e186976db0f5d453d1cdd31473c85a6a161affb6"}, + {file = "requests-2.32.5.tar.gz", hash = "sha256:dbba0bac56e100853db0ea71b82b4dfd5fe2bf6d3754a8893c3af500cec7d7cf"}, ] [package.dependencies] @@ -2526,20 +2725,19 @@ idna2008 = ["idna"] [[package]] name = "rich" -version = "14.0.0" +version = "14.3.3" description = "Render rich text, tables, progress bars, syntax highlighting, markdown and more to the terminal" optional = false python-versions = ">=3.8.0" groups = ["dev"] files = [ - {file = "rich-14.0.0-py3-none-any.whl", hash = "sha256:1c9491e1951aac09caffd42f448ee3d04e58923ffe14993f6e83068dc395d7e0"}, - {file = "rich-14.0.0.tar.gz", hash = "sha256:82f1bc23a6a21ebca4ae0c45af9bdbc492ed20231dcb63f297d6d1021a9d5725"}, + {file = "rich-14.3.3-py3-none-any.whl", hash = "sha256:793431c1f8619afa7d3b52b2cdec859562b950ea0d4b6b505397612db8d5362d"}, + {file = "rich-14.3.3.tar.gz", hash = "sha256:b8daa0b9e4eef54dd8cf7c86c03713f53241884e814f4e2f5fb342fe520f639b"}, ] [package.dependencies] markdown-it-py = ">=2.2.0" pygments = ">=2.13.0,<3.0.0" -typing-extensions = {version = ">=4.0.0,<5.0", markers = "python_version < \"3.11\""} [package.extras] jupyter = ["ipywidgets (>=7.5.1,<9)"] @@ -2700,15 +2898,15 @@ files = [ [[package]] name = "secretstorage" -version = "3.3.3" +version = "3.5.0" description = "Python bindings to FreeDesktop.org Secret Service API" optional = false -python-versions = ">=3.6" +python-versions = ">=3.10" groups = ["dev"] markers = "platform_machine != \"ppc64le\" and platform_machine != \"s390x\" and sys_platform == \"linux\"" files = [ - {file = "SecretStorage-3.3.3-py3-none-any.whl", hash = "sha256:f356e6628222568e3af06f2eba8df495efa13b3b63081dafd4f7d9a7b7bc9f99"}, - {file = "SecretStorage-3.3.3.tar.gz", hash = "sha256:2403533ef369eca6d2ba81718576c5e0f564d5cca1b58f73a8b23e7d4eeebd77"}, + {file = "secretstorage-3.5.0-py3-none-any.whl", hash = "sha256:0ce65888c0725fcb2c5bc0fdb8e5438eece02c523557ea40ce0703c266248137"}, + {file = "secretstorage-3.5.0.tar.gz", hash = "sha256:f04b8e4689cbce351744d5537bf6b1329c6fc68f91fa666f60a380edddcd11be"}, ] [package.dependencies] @@ -2733,15 +2931,15 @@ doc = ["Sphinx", "sphinx-rtd-theme"] [[package]] name = "sentry-sdk" -version = "2.46.0" +version = "2.54.0" description = "Python client for Sentry (https://sentry.io)" optional = true python-versions = ">=3.6" groups = ["main"] -markers = "extra == \"all\" or extra == \"sentry\"" +markers = "extra == \"sentry\" or extra == \"all\"" files = [ - {file = "sentry_sdk-2.46.0-py2.py3-none-any.whl", hash = "sha256:4eeeb60198074dff8d066ea153fa6f241fef1668c10900ea53a4200abc8da9b1"}, - {file = "sentry_sdk-2.46.0.tar.gz", hash = "sha256:91821a23460725734b7741523021601593f35731808afc0bb2ba46c27b8acd91"}, + {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"}, ] [package.dependencies] @@ -2822,24 +3020,24 @@ tests = ["coverage[toml] (>=5.0.2)", "pytest"] [[package]] name = "setuptools" -version = "78.1.1" +version = "82.0.0" description = "Easily download, build, install, upgrade, and uninstall Python packages" optional = false python-versions = ">=3.9" -groups = ["main", "dev"] +groups = ["main"] files = [ - {file = "setuptools-78.1.1-py3-none-any.whl", hash = "sha256:c3a9c4211ff4c309edb8b8c4f1cbfa7ae324c4ba9f91ff254e3d305b9fd54561"}, - {file = "setuptools-78.1.1.tar.gz", hash = "sha256:fcc17fd9cd898242f6b4adfaca46137a9edef687f43e6f78469692a5e70d851d"}, + {file = "setuptools-82.0.0-py3-none-any.whl", hash = "sha256:70b18734b607bd1da571d097d236cfcfacaf01de45717d59e6e04b96877532e0"}, + {file = "setuptools-82.0.0.tar.gz", hash = "sha256:22e0a2d69474c6ae4feb01951cb69d515ed23728cf96d05513d36e42b62b37cb"}, ] [package.extras] -check = ["pytest-checkdocs (>=2.4)", "pytest-ruff (>=0.2.1) ; sys_platform != \"cygwin\"", "ruff (>=0.8.0) ; sys_platform != \"cygwin\""] +check = ["pytest-checkdocs (>=2.4)", "pytest-ruff (>=0.2.1) ; sys_platform != \"cygwin\"", "ruff (>=0.13.0) ; sys_platform != \"cygwin\""] core = ["importlib_metadata (>=6) ; python_version < \"3.10\"", "jaraco.functools (>=4)", "jaraco.text (>=3.7)", "more_itertools", "more_itertools (>=8.8)", "packaging (>=24.2)", "platformdirs (>=4.2.2)", "tomli (>=2.0.1) ; python_version < \"3.11\"", "wheel (>=0.43.0)"] cover = ["pytest-cov"] doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "pygments-github-lexers (==0.0.5)", "pyproject-hooks (!=1.1)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-favicon", "sphinx-inline-tabs", "sphinx-lint", "sphinx-notfound-page (>=1,<2)", "sphinx-reredirects", "sphinxcontrib-towncrier", "towncrier (<24.7)"] enabler = ["pytest-enabler (>=2.2)"] test = ["build[virtualenv] (>=1.0.3)", "filelock (>=3.4.0)", "ini2toml[lite] (>=0.14)", "jaraco.develop (>=7.21) ; python_version >= \"3.9\" and sys_platform != \"cygwin\"", "jaraco.envs (>=2.2)", "jaraco.path (>=3.7.2)", "jaraco.test (>=5.5)", "packaging (>=24.2)", "pip (>=19.1)", "pyproject-hooks (!=1.1)", "pytest (>=6,!=8.1.*)", "pytest-home (>=0.5)", "pytest-perf ; sys_platform != \"cygwin\"", "pytest-subprocess", "pytest-timeout", "pytest-xdist (>=3)", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel (>=0.44.0)"] -type = ["importlib_metadata (>=7.0.2) ; python_version < \"3.10\"", "jaraco.develop (>=7.21) ; sys_platform != \"cygwin\"", "mypy (==1.14.*)", "pytest-mypy"] +type = ["importlib_metadata (>=7.0.2) ; python_version < \"3.10\"", "jaraco.develop (>=7.21) ; sys_platform != \"cygwin\"", "mypy (==1.18.*)", "pytest-mypy"] [[package]] name = "setuptools-rust" @@ -2879,26 +3077,26 @@ dev = ["typing-extensions (>=3.5)"] [[package]] name = "six" -version = "1.16.0" +version = "1.17.0" description = "Python 2 and 3 compatibility utilities" optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*" +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7" groups = ["main"] files = [ - {file = "six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"}, - {file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"}, + {file = "six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274"}, + {file = "six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81"}, ] [[package]] name = "smmap" -version = "5.0.0" +version = "5.0.2" description = "A pure Python implementation of a sliding window memory map manager" optional = false -python-versions = ">=3.6" +python-versions = ">=3.7" groups = ["dev"] files = [ - {file = "smmap-5.0.0-py3-none-any.whl", hash = "sha256:2aba19d6a040e78d8b09de5c57e96207b09ed71d8e55ce0959eeee6c8e190d94"}, - {file = "smmap-5.0.0.tar.gz", hash = "sha256:c840e62059cd3be204b0c9c9f74be2c09d5648eddd4580d9314c3ecde0b30936"}, + {file = "smmap-5.0.2-py3-none-any.whl", hash = "sha256:b30115f0def7d7531d22a0fb6502488d879e75b260a9db4d0819cfb25403af5e"}, + {file = "smmap-5.0.2.tar.gz", hash = "sha256:26ea65a03958fa0c8a1c7e8c7a58fdc77221b8910f6be2131affade476898ad5"}, ] [[package]] @@ -2915,19 +3113,20 @@ files = [ [[package]] name = "sqlglot" -version = "28.0.0" +version = "29.0.1" description = "An easily customizable SQL parser and transpiler" optional = false python-versions = ">=3.9" groups = ["dev"] files = [ - {file = "sqlglot-28.0.0-py3-none-any.whl", hash = "sha256:ac1778e7fa4812f4f7e5881b260632fc167b00ca4c1226868891fb15467122e4"}, - {file = "sqlglot-28.0.0.tar.gz", hash = "sha256:cc9a651ef4182e61dac58aa955e5fb21845a5865c6a4d7d7b5a7857450285ad4"}, + {file = "sqlglot-29.0.1-py3-none-any.whl", hash = "sha256:06a473ea6c2b3632ac67bd38e687a6860265bf4156e66b54adeda15d07f00c65"}, + {file = "sqlglot-29.0.1.tar.gz", hash = "sha256:0010b4f77fb996c8d25dd4b16f3654e6da163ff1866ceabc70b24e791c203048"}, ] [package.extras] -dev = ["duckdb (>=0.6)", "maturin (>=1.4,<2.0)", "mypy", "pandas", "pandas-stubs", "pdoc", "pre-commit", "pyperf", "python-dateutil", "pytz", "ruff (==0.7.2)", "types-python-dateutil", "types-pytz", "typing_extensions"] -rs = ["sqlglotrs (==0.7.3)"] +c = ["sqlglotc"] +dev = ["duckdb (>=0.6)", "mypy", "pandas", "pandas-stubs", "pdoc", "pre-commit", "pyperf", "python-dateutil", "pytz", "ruff (==0.7.2)", "types-python-dateutil", "types-pytz", "typing_extensions"] +rs = ["sqlglotrs (==0.13.0)"] [[package]] name = "systemd-python" @@ -2948,7 +3147,7 @@ description = "Tornado IOLoop Backed Concurrent Futures" optional = true python-versions = "*" groups = ["main"] -markers = "extra == \"all\" or extra == \"opentracing\"" +markers = "extra == \"opentracing\" or extra == \"all\"" files = [ {file = "threadloop-1.0.2-py2-none-any.whl", hash = "sha256:5c90dbefab6ffbdba26afb4829d2a9df8275d13ac7dc58dccb0e279992679599"}, {file = "threadloop-1.0.2.tar.gz", hash = "sha256:8b180aac31013de13c2ad5c834819771992d350267bddb854613ae77ef571944"}, @@ -2959,19 +3158,16 @@ tornado = "*" [[package]] name = "thrift" -version = "0.16.0" +version = "0.22.0" description = "Python bindings for the Apache Thrift RPC system" optional = true python-versions = "*" groups = ["main"] -markers = "extra == \"all\" or extra == \"opentracing\"" +markers = "extra == \"opentracing\" or extra == \"all\"" files = [ - {file = "thrift-0.16.0.tar.gz", hash = "sha256:2b5b6488fcded21f9d312aa23c9ff6a0195d0f6ae26ddbd5ad9e3e25dfc14408"}, + {file = "thrift-0.22.0.tar.gz", hash = "sha256:42e8276afbd5f54fe1d364858b6877bc5e5a4a5ed69f6a005b94ca4918fe1466"}, ] -[package.dependencies] -six = ">=1.7.2" - [package.extras] all = ["tornado (>=4.0)", "twisted"] tornado = ["tornado (>=4.0)"] @@ -2979,78 +3175,83 @@ twisted = ["twisted"] [[package]] name = "tomli" -version = "2.3.0" +version = "2.4.0" description = "A lil' TOML parser" optional = false python-versions = ">=3.8" groups = ["main", "dev"] files = [ - {file = "tomli-2.3.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:88bd15eb972f3664f5ed4b57c1634a97153b4bac4479dcb6a495f41921eb7f45"}, - {file = "tomli-2.3.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:883b1c0d6398a6a9d29b508c331fa56adbcdff647f6ace4dfca0f50e90dfd0ba"}, - {file = "tomli-2.3.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d1381caf13ab9f300e30dd8feadb3de072aeb86f1d34a8569453ff32a7dea4bf"}, - {file = "tomli-2.3.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a0e285d2649b78c0d9027570d4da3425bdb49830a6156121360b3f8511ea3441"}, - {file = "tomli-2.3.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:0a154a9ae14bfcf5d8917a59b51ffd5a3ac1fd149b71b47a3a104ca4edcfa845"}, - {file = "tomli-2.3.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:74bf8464ff93e413514fefd2be591c3b0b23231a77f901db1eb30d6f712fc42c"}, - {file = "tomli-2.3.0-cp311-cp311-win32.whl", hash = "sha256:00b5f5d95bbfc7d12f91ad8c593a1659b6387b43f054104cda404be6bda62456"}, - {file = "tomli-2.3.0-cp311-cp311-win_amd64.whl", hash = "sha256:4dc4ce8483a5d429ab602f111a93a6ab1ed425eae3122032db7e9acf449451be"}, - {file = "tomli-2.3.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:d7d86942e56ded512a594786a5ba0a5e521d02529b3826e7761a05138341a2ac"}, - {file = "tomli-2.3.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:73ee0b47d4dad1c5e996e3cd33b8a76a50167ae5f96a2607cbe8cc773506ab22"}, - {file = "tomli-2.3.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:792262b94d5d0a466afb5bc63c7daa9d75520110971ee269152083270998316f"}, - {file = "tomli-2.3.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4f195fe57ecceac95a66a75ac24d9d5fbc98ef0962e09b2eddec5d39375aae52"}, - {file = "tomli-2.3.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e31d432427dcbf4d86958c184b9bfd1e96b5b71f8eb17e6d02531f434fd335b8"}, - {file = "tomli-2.3.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:7b0882799624980785240ab732537fcfc372601015c00f7fc367c55308c186f6"}, - {file = "tomli-2.3.0-cp312-cp312-win32.whl", hash = "sha256:ff72b71b5d10d22ecb084d345fc26f42b5143c5533db5e2eaba7d2d335358876"}, - {file = "tomli-2.3.0-cp312-cp312-win_amd64.whl", hash = "sha256:1cb4ed918939151a03f33d4242ccd0aa5f11b3547d0cf30f7c74a408a5b99878"}, - {file = "tomli-2.3.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:5192f562738228945d7b13d4930baffda67b69425a7f0da96d360b0a3888136b"}, - {file = "tomli-2.3.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:be71c93a63d738597996be9528f4abe628d1adf5e6eb11607bc8fe1a510b5dae"}, - {file = "tomli-2.3.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c4665508bcbac83a31ff8ab08f424b665200c0e1e645d2bd9ab3d3e557b6185b"}, - {file = "tomli-2.3.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4021923f97266babc6ccab9f5068642a0095faa0a51a246a6a02fccbb3514eaf"}, - {file = "tomli-2.3.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a4ea38c40145a357d513bffad0ed869f13c1773716cf71ccaa83b0fa0cc4e42f"}, - {file = "tomli-2.3.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:ad805ea85eda330dbad64c7ea7a4556259665bdf9d2672f5dccc740eb9d3ca05"}, - {file = "tomli-2.3.0-cp313-cp313-win32.whl", hash = "sha256:97d5eec30149fd3294270e889b4234023f2c69747e555a27bd708828353ab606"}, - {file = "tomli-2.3.0-cp313-cp313-win_amd64.whl", hash = "sha256:0c95ca56fbe89e065c6ead5b593ee64b84a26fca063b5d71a1122bf26e533999"}, - {file = "tomli-2.3.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:cebc6fe843e0733ee827a282aca4999b596241195f43b4cc371d64fc6639da9e"}, - {file = "tomli-2.3.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:4c2ef0244c75aba9355561272009d934953817c49f47d768070c3c94355c2aa3"}, - {file = "tomli-2.3.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c22a8bf253bacc0cf11f35ad9808b6cb75ada2631c2d97c971122583b129afbc"}, - {file = "tomli-2.3.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0eea8cc5c5e9f89c9b90c4896a8deefc74f518db5927d0e0e8d4a80953d774d0"}, - {file = "tomli-2.3.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:b74a0e59ec5d15127acdabd75ea17726ac4c5178ae51b85bfe39c4f8a278e879"}, - {file = "tomli-2.3.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:b5870b50c9db823c595983571d1296a6ff3e1b88f734a4c8f6fc6188397de005"}, - {file = "tomli-2.3.0-cp314-cp314-win32.whl", hash = "sha256:feb0dacc61170ed7ab602d3d972a58f14ee3ee60494292d384649a3dc38ef463"}, - {file = "tomli-2.3.0-cp314-cp314-win_amd64.whl", hash = "sha256:b273fcbd7fc64dc3600c098e39136522650c49bca95df2d11cf3b626422392c8"}, - {file = "tomli-2.3.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:940d56ee0410fa17ee1f12b817b37a4d4e4dc4d27340863cc67236c74f582e77"}, - {file = "tomli-2.3.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:f85209946d1fe94416debbb88d00eb92ce9cd5266775424ff81bc959e001acaf"}, - {file = "tomli-2.3.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a56212bdcce682e56b0aaf79e869ba5d15a6163f88d5451cbde388d48b13f530"}, - {file = "tomli-2.3.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c5f3ffd1e098dfc032d4d3af5c0ac64f6d286d98bc148698356847b80fa4de1b"}, - {file = "tomli-2.3.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:5e01decd096b1530d97d5d85cb4dff4af2d8347bd35686654a004f8dea20fc67"}, - {file = "tomli-2.3.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:8a35dd0e643bb2610f156cca8db95d213a90015c11fee76c946aa62b7ae7e02f"}, - {file = "tomli-2.3.0-cp314-cp314t-win32.whl", hash = "sha256:a1f7f282fe248311650081faafa5f4732bdbfef5d45fe3f2e702fbc6f2d496e0"}, - {file = "tomli-2.3.0-cp314-cp314t-win_amd64.whl", hash = "sha256:70a251f8d4ba2d9ac2542eecf008b3c8a9fc5c3f9f02c56a9d7952612be2fdba"}, - {file = "tomli-2.3.0-py3-none-any.whl", hash = "sha256:e95b1af3c5b07d9e643909b5abbec77cd9f1217e6d0bca72b0234736b9fb1f1b"}, - {file = "tomli-2.3.0.tar.gz", hash = "sha256:64be704a875d2a59753d80ee8a533c3fe183e3f06807ff7dc2232938ccb01549"}, + {file = "tomli-2.4.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:b5ef256a3fd497d4973c11bf142e9ed78b150d36f5773f1ca6088c230ffc5867"}, + {file = "tomli-2.4.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:5572e41282d5268eb09a697c89a7bee84fae66511f87533a6f88bd2f7b652da9"}, + {file = "tomli-2.4.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:551e321c6ba03b55676970b47cb1b73f14a0a4dce6a3e1a9458fd6d921d72e95"}, + {file = "tomli-2.4.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5e3f639a7a8f10069d0e15408c0b96a2a828cfdec6fca05296ebcdcc28ca7c76"}, + {file = "tomli-2.4.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1b168f2731796b045128c45982d3a4874057626da0e2ef1fdd722848b741361d"}, + {file = "tomli-2.4.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:133e93646ec4300d651839d382d63edff11d8978be23da4cc106f5a18b7d0576"}, + {file = "tomli-2.4.0-cp311-cp311-win32.whl", hash = "sha256:b6c78bdf37764092d369722d9946cb65b8767bfa4110f902a1b2542d8d173c8a"}, + {file = "tomli-2.4.0-cp311-cp311-win_amd64.whl", hash = "sha256:d3d1654e11d724760cdb37a3d7691f0be9db5fbdaef59c9f532aabf87006dbaa"}, + {file = "tomli-2.4.0-cp311-cp311-win_arm64.whl", hash = "sha256:cae9c19ed12d4e8f3ebf46d1a75090e4c0dc16271c5bce1c833ac168f08fb614"}, + {file = "tomli-2.4.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:920b1de295e72887bafa3ad9f7a792f811847d57ea6b1215154030cf131f16b1"}, + {file = "tomli-2.4.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:7d6d9a4aee98fac3eab4952ad1d73aee87359452d1c086b5ceb43ed02ddb16b8"}, + {file = "tomli-2.4.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:36b9d05b51e65b254ea6c2585b59d2c4cb91c8a3d91d0ed0f17591a29aaea54a"}, + {file = "tomli-2.4.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1c8a885b370751837c029ef9bc014f27d80840e48bac415f3412e6593bbc18c1"}, + {file = "tomli-2.4.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8768715ffc41f0008abe25d808c20c3d990f42b6e2e58305d5da280ae7d1fa3b"}, + {file = "tomli-2.4.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:7b438885858efd5be02a9a133caf5812b8776ee0c969fea02c45e8e3f296ba51"}, + {file = "tomli-2.4.0-cp312-cp312-win32.whl", hash = "sha256:0408e3de5ec77cc7f81960c362543cbbd91ef883e3138e81b729fc3eea5b9729"}, + {file = "tomli-2.4.0-cp312-cp312-win_amd64.whl", hash = "sha256:685306e2cc7da35be4ee914fd34ab801a6acacb061b6a7abca922aaf9ad368da"}, + {file = "tomli-2.4.0-cp312-cp312-win_arm64.whl", hash = "sha256:5aa48d7c2356055feef06a43611fc401a07337d5b006be13a30f6c58f869e3c3"}, + {file = "tomli-2.4.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:84d081fbc252d1b6a982e1870660e7330fb8f90f676f6e78b052ad4e64714bf0"}, + {file = "tomli-2.4.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:9a08144fa4cba33db5255f9b74f0b89888622109bd2776148f2597447f92a94e"}, + {file = "tomli-2.4.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c73add4bb52a206fd0c0723432db123c0c75c280cbd67174dd9d2db228ebb1b4"}, + {file = "tomli-2.4.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1fb2945cbe303b1419e2706e711b7113da57b7db31ee378d08712d678a34e51e"}, + {file = "tomli-2.4.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:bbb1b10aa643d973366dc2cb1ad94f99c1726a02343d43cbc011edbfac579e7c"}, + {file = "tomli-2.4.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4cbcb367d44a1f0c2be408758b43e1ffb5308abe0ea222897d6bfc8e8281ef2f"}, + {file = "tomli-2.4.0-cp313-cp313-win32.whl", hash = "sha256:7d49c66a7d5e56ac959cb6fc583aff0651094ec071ba9ad43df785abc2320d86"}, + {file = "tomli-2.4.0-cp313-cp313-win_amd64.whl", hash = "sha256:3cf226acb51d8f1c394c1b310e0e0e61fecdd7adcb78d01e294ac297dd2e7f87"}, + {file = "tomli-2.4.0-cp313-cp313-win_arm64.whl", hash = "sha256:d20b797a5c1ad80c516e41bc1fb0443ddb5006e9aaa7bda2d71978346aeb9132"}, + {file = "tomli-2.4.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:26ab906a1eb794cd4e103691daa23d95c6919cc2fa9160000ac02370cc9dd3f6"}, + {file = "tomli-2.4.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:20cedb4ee43278bc4f2fee6cb50daec836959aadaf948db5172e776dd3d993fc"}, + {file = "tomli-2.4.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:39b0b5d1b6dd03684b3fb276407ebed7090bbec989fa55838c98560c01113b66"}, + {file = "tomli-2.4.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a26d7ff68dfdb9f87a016ecfd1e1c2bacbe3108f4e0f8bcd2228ef9a766c787d"}, + {file = "tomli-2.4.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:20ffd184fb1df76a66e34bd1b36b4a4641bd2b82954befa32fe8163e79f1a702"}, + {file = "tomli-2.4.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:75c2f8bbddf170e8effc98f5e9084a8751f8174ea6ccf4fca5398436e0320bc8"}, + {file = "tomli-2.4.0-cp314-cp314-win32.whl", hash = "sha256:31d556d079d72db7c584c0627ff3a24c5d3fb4f730221d3444f3efb1b2514776"}, + {file = "tomli-2.4.0-cp314-cp314-win_amd64.whl", hash = "sha256:43e685b9b2341681907759cf3a04e14d7104b3580f808cfde1dfdb60ada85475"}, + {file = "tomli-2.4.0-cp314-cp314-win_arm64.whl", hash = "sha256:3d895d56bd3f82ddd6faaff993c275efc2ff38e52322ea264122d72729dca2b2"}, + {file = "tomli-2.4.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:5b5807f3999fb66776dbce568cc9a828544244a8eb84b84b9bafc080c99597b9"}, + {file = "tomli-2.4.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c084ad935abe686bd9c898e62a02a19abfc9760b5a79bc29644463eaf2840cb0"}, + {file = "tomli-2.4.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0f2e3955efea4d1cfbcb87bc321e00dc08d2bcb737fd1d5e398af111d86db5df"}, + {file = "tomli-2.4.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0e0fe8a0b8312acf3a88077a0802565cb09ee34107813bba1c7cd591fa6cfc8d"}, + {file = "tomli-2.4.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:413540dce94673591859c4c6f794dfeaa845e98bf35d72ed59636f869ef9f86f"}, + {file = "tomli-2.4.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:0dc56fef0e2c1c470aeac5b6ca8cc7b640bb93e92d9803ddaf9ea03e198f5b0b"}, + {file = "tomli-2.4.0-cp314-cp314t-win32.whl", hash = "sha256:d878f2a6707cc9d53a1be1414bbb419e629c3d6e67f69230217bb663e76b5087"}, + {file = "tomli-2.4.0-cp314-cp314t-win_amd64.whl", hash = "sha256:2add28aacc7425117ff6364fe9e06a183bb0251b03f986df0e78e974047571fd"}, + {file = "tomli-2.4.0-cp314-cp314t-win_arm64.whl", hash = "sha256:2b1e3b80e1d5e52e40e9b924ec43d81570f0e7d09d11081b797bc4692765a3d4"}, + {file = "tomli-2.4.0-py3-none-any.whl", hash = "sha256:1f776e7d669ebceb01dee46484485f43a4048746235e683bcdffacdf1fb4785a"}, + {file = "tomli-2.4.0.tar.gz", hash = "sha256:aa89c3f6c277dd275d8e243ad24f3b5e701491a860d5121f2cdd399fbb31fc9c"}, ] markers = {main = "python_version < \"3.14\""} [[package]] name = "tornado" -version = "6.5" +version = "6.5.4" 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 == \"all\" or extra == \"opentracing\"" +markers = "extra == \"opentracing\" or extra == \"all\"" files = [ - {file = "tornado-6.5-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:f81067dad2e4443b015368b24e802d0083fecada4f0a4572fdb72fc06e54a9a6"}, - {file = "tornado-6.5-cp39-abi3-macosx_10_9_x86_64.whl", hash = "sha256:9ac1cbe1db860b3cbb251e795c701c41d343f06a96049d6274e7c77559117e41"}, - {file = "tornado-6.5-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7c625b9d03f1fb4d64149c47d0135227f0434ebb803e2008040eb92906b0105a"}, - {file = "tornado-6.5-cp39-abi3-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9a0d8d2309faf015903080fb5bdd969ecf9aa5ff893290845cf3fd5b2dd101bc"}, - {file = "tornado-6.5-cp39-abi3-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:03576ab51e9b1677e4cdaae620d6700d9823568b7939277e4690fe4085886c55"}, - {file = "tornado-6.5-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:ab75fe43d0e1b3a5e3ceddb2a611cb40090dd116a84fc216a07a298d9e000471"}, - {file = "tornado-6.5-cp39-abi3-musllinux_1_2_i686.whl", hash = "sha256:119c03f440a832128820e87add8a175d211b7f36e7ee161c631780877c28f4fb"}, - {file = "tornado-6.5-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:231f2193bb4c28db2bdee9e57bc6ca0cd491f345cd307c57d79613b058e807e0"}, - {file = "tornado-6.5-cp39-abi3-win32.whl", hash = "sha256:fd20c816e31be1bbff1f7681f970bbbd0bb241c364220140228ba24242bcdc59"}, - {file = "tornado-6.5-cp39-abi3-win_amd64.whl", hash = "sha256:007f036f7b661e899bd9ef3fa5f87eb2cb4d1b2e7d67368e778e140a2f101a7a"}, - {file = "tornado-6.5-cp39-abi3-win_arm64.whl", hash = "sha256:542e380658dcec911215c4820654662810c06ad872eefe10def6a5e9b20e9633"}, - {file = "tornado-6.5.tar.gz", hash = "sha256:c70c0a26d5b2d85440e4debd14a8d0b463a0cf35d92d3af05f5f1ffa8675c826"}, + {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"}, ] [[package]] @@ -3171,7 +3372,7 @@ description = "non-blocking redis client for python" optional = true python-versions = "*" groups = ["main"] -markers = "extra == \"all\" or extra == \"redis\"" +markers = "extra == \"redis\" or extra == \"all\"" files = [ {file = "txredisapi-1.4.11-py3-none-any.whl", hash = "sha256:ac64d7a9342b58edca13ef267d4fa7637c1aa63f8595e066801c1e8b56b22d0b"}, {file = "txredisapi-1.4.11.tar.gz", hash = "sha256:3eb1af99aefdefb59eb877b1dd08861efad60915e30ad5bf3d5bf6c5cedcdbc6"}, @@ -3198,14 +3399,14 @@ types-html5lib = "*" [[package]] name = "types-cffi" -version = "1.16.0.20240331" +version = "1.17.0.20250915" description = "Typing stubs for cffi" optional = false -python-versions = ">=3.8" +python-versions = ">=3.9" groups = ["dev"] files = [ - {file = "types-cffi-1.16.0.20240331.tar.gz", hash = "sha256:b8b20d23a2b89cfed5f8c5bc53b0cb8677c3aac6d970dbc771e28b9c698f5dee"}, - {file = "types_cffi-1.16.0.20240331-py3-none-any.whl", hash = "sha256:a363e5ea54a4eb6a4a105d800685fde596bc318089b025b27dee09849fe41ff0"}, + {file = "types_cffi-1.17.0.20250915-py3-none-any.whl", hash = "sha256:cef4af1116c83359c11bb4269283c50f0688e9fc1d7f0eeb390f3661546da52c"}, + {file = "types_cffi-1.17.0.20250915.tar.gz", hash = "sha256:4362e20368f78dabd5c56bca8004752cc890e07a71605d9e0d9e069dbaac8c06"}, ] [package.dependencies] @@ -3213,26 +3414,29 @@ types-setuptools = "*" [[package]] name = "types-html5lib" -version = "1.1.11.20240228" +version = "1.1.11.20251117" description = "Typing stubs for html5lib" optional = false -python-versions = ">=3.8" +python-versions = ">=3.9" groups = ["dev"] files = [ - {file = "types-html5lib-1.1.11.20240228.tar.gz", hash = "sha256:22736b7299e605ec4ba539d48691e905fd0c61c3ea610acc59922232dc84cede"}, - {file = "types_html5lib-1.1.11.20240228-py3-none-any.whl", hash = "sha256:af5de0125cb0fe5667543b158db83849b22e25c0e36c9149836b095548bf1020"}, + {file = "types_html5lib-1.1.11.20251117-py3-none-any.whl", hash = "sha256:2a3fc935de788a4d2659f4535002a421e05bea5e172b649d33232e99d4272d08"}, + {file = "types_html5lib-1.1.11.20251117.tar.gz", hash = "sha256:1a6a3ac5394aa12bf547fae5d5eff91dceec46b6d07c4367d9b39a37f42f201a"}, ] +[package.dependencies] +types-webencodings = "*" + [[package]] name = "types-jsonschema" -version = "4.25.1.20251009" +version = "4.26.0.20260202" description = "Typing stubs for jsonschema" optional = false python-versions = ">=3.9" groups = ["dev"] files = [ - {file = "types_jsonschema-4.25.1.20251009-py3-none-any.whl", hash = "sha256:f30b329037b78e7a60146b1146feb0b6fb0b71628637584409bada83968dad3e"}, - {file = "types_jsonschema-4.25.1.20251009.tar.gz", hash = "sha256:75d0f5c5dd18dc23b664437a0c1a625743e8d2e665ceaf3aecb29841f3a5f97f"}, + {file = "types_jsonschema-4.26.0.20260202-py3-none-any.whl", hash = "sha256:41c95343abc4de9264e333a55e95dfb4d401e463856d0164eec9cb182e8746da"}, + {file = "types_jsonschema-4.26.0.20260202.tar.gz", hash = "sha256:29831baa4308865a9aec547a61797a06fc152b0dac8dddd531e002f32265cb07"}, ] [package.dependencies] @@ -3240,14 +3444,14 @@ referencing = "*" [[package]] name = "types-netaddr" -version = "1.3.0.20251108" +version = "1.3.0.20260130" description = "Typing stubs for netaddr" optional = false python-versions = ">=3.9" groups = ["dev"] files = [ - {file = "types_netaddr-1.3.0.20251108-py3-none-any.whl", hash = "sha256:1699b3aae860b8754e8ba7fb426905287065c9dbce05d019c25b630ae2ba66c5"}, - {file = "types_netaddr-1.3.0.20251108.tar.gz", hash = "sha256:2895d8a48eb71ba0ebecf74c6cebaddfa0f97199835ebb406b5aae1725e06ac1"}, + {file = "types_netaddr-1.3.0.20260130-py3-none-any.whl", hash = "sha256:ec1b2de102bda98d063f97d87de5370778d0d955ae2928a9e3465554f90c9a92"}, + {file = "types_netaddr-1.3.0.20260130.tar.gz", hash = "sha256:13d9805c8d5eeb27406938de9c2d9c447c032358c8538c11f796ab382d17d54d"}, ] [[package]] @@ -3276,14 +3480,14 @@ files = [ [[package]] name = "types-psycopg2" -version = "2.9.21.20251012" +version = "2.9.21.20260223" description = "Typing stubs for psycopg2" optional = false -python-versions = ">=3.9" +python-versions = ">=3.10" groups = ["dev"] files = [ - {file = "types_psycopg2-2.9.21.20251012-py3-none-any.whl", hash = "sha256:712bad5c423fe979e357edbf40a07ca40ef775d74043de72bd4544ca328cc57e"}, - {file = "types_psycopg2-2.9.21.20251012.tar.gz", hash = "sha256:4cdafd38927da0cfde49804f39ab85afd9c6e9c492800e42f1f0c1a1b0312935"}, + {file = "types_psycopg2-2.9.21.20260223-py3-none-any.whl", hash = "sha256:c6228ade72d813b0624f4c03feeb89471950ac27cd0506b5debed6f053086bc8"}, + {file = "types_psycopg2-2.9.21.20260223.tar.gz", hash = "sha256:78ed70de2e56bc6b5c26c8c1da8e9af54e49fdc3c94d1504609f3519e2b84f02"}, ] [[package]] @@ -3316,14 +3520,14 @@ files = [ [[package]] name = "types-requests" -version = "2.32.4.20250913" +version = "2.32.4.20260107" description = "Typing stubs for requests" optional = false python-versions = ">=3.9" groups = ["dev"] files = [ - {file = "types_requests-2.32.4.20250913-py3-none-any.whl", hash = "sha256:78c9c1fffebbe0fa487a418e0fa5252017e9c60d1a2da394077f1780f655d7e1"}, - {file = "types_requests-2.32.4.20250913.tar.gz", hash = "sha256:abd6d4f9ce3a9383f269775a9835a4c24e5cd6b9f647d64f88aa4613c33def5d"}, + {file = "types_requests-2.32.4.20260107-py3-none-any.whl", hash = "sha256:b703fe72f8ce5b31ef031264fe9395cac8f46a04661a79f7ed31a80fb308730d"}, + {file = "types_requests-2.32.4.20260107.tar.gz", hash = "sha256:018a11ac158f801bfa84857ddec1650750e393df8a004a8a9ae2a9bec6fcb24f"}, ] [package.dependencies] @@ -3331,14 +3535,26 @@ urllib3 = ">=2" [[package]] name = "types-setuptools" -version = "80.9.0.20250822" +version = "82.0.0.20260210" description = "Typing stubs for setuptools" optional = false +python-versions = ">=3.10" +groups = ["dev"] +files = [ + {file = "types_setuptools-82.0.0.20260210-py3-none-any.whl", hash = "sha256:5124a7daf67f195c6054e0f00f1d97c69caad12fdcf9113eba33eff0bce8cd2b"}, + {file = "types_setuptools-82.0.0.20260210.tar.gz", hash = "sha256:d9719fbbeb185254480ade1f25327c4654f8c00efda3fec36823379cebcdee58"}, +] + +[[package]] +name = "types-webencodings" +version = "0.5.0.20251108" +description = "Typing stubs for webencodings" +optional = false python-versions = ">=3.9" groups = ["dev"] files = [ - {file = "types_setuptools-80.9.0.20250822-py3-none-any.whl", hash = "sha256:53bf881cb9d7e46ed12c76ef76c0aaf28cfe6211d3fab12e0b83620b1a8642c3"}, - {file = "types_setuptools-80.9.0.20250822.tar.gz", hash = "sha256:070ea7716968ec67a84c7f7768d9952ff24d28b65b6594797a464f1b3066f965"}, + {file = "types_webencodings-0.5.0.20251108-py3-none-any.whl", hash = "sha256:e21f81ff750795faffddaffd70a3d8bfff77d006f22c27e393eb7812586249d8"}, + {file = "types_webencodings-0.5.0.20251108.tar.gz", hash = "sha256:2378e2ceccced3d41bb5e21387586e7b5305e11519fc6b0659c629f23b2e5de4"}, ] [[package]] @@ -3404,7 +3620,7 @@ version = "0.5.1" description = "Character encoding aliases for legacy web content" optional = false python-versions = "*" -groups = ["main", "dev"] +groups = ["main"] files = [ {file = "webencodings-0.5.1-py2.py3-none-any.whl", hash = "sha256:a0af1213f3c2226497a97e2b3aa01a7e4bee4f403f95be16fc9acd2947514a78"}, {file = "webencodings-0.5.1.tar.gz", hash = "sha256:b36a1c245f2d304965eb4e0a82848379241dc04b865afcc4aab16748587e1923"}, @@ -3412,15 +3628,15 @@ files = [ [[package]] name = "xmlschema" -version = "2.4.0" +version = "2.5.1" description = "An XML Schema validator and decoder" optional = true python-versions = ">=3.7" groups = ["main"] -markers = "extra == \"all\" or extra == \"saml2\"" +markers = "extra == \"saml2\" or extra == \"all\"" files = [ - {file = "xmlschema-2.4.0-py3-none-any.whl", hash = "sha256:dc87be0caaa61f42649899189aab2fd8e0d567f2cf548433ba7b79278d231a4a"}, - {file = "xmlschema-2.4.0.tar.gz", hash = "sha256:d74cd0c10866ac609e1ef94a5a69b018ad16e39077bc6393408b40c6babee793"}, + {file = "xmlschema-2.5.1-py3-none-any.whl", hash = "sha256:ec2b2a15c8896c1fcd14dcee34ca30032b99456c3c43ce793fdb9dca2fb4b869"}, + {file = "xmlschema-2.5.1.tar.gz", hash = "sha256:4f7497de6c8b6dc2c28ad7b9ed6e21d186f4afe248a5bea4f54eedab4da44083"}, ] [package.dependencies] @@ -3433,79 +3649,80 @@ docs = ["Sphinx", "elementpath (>=4.1.5,<5.0.0)", "jinja2", "sphinx-rtd-theme"] [[package]] name = "zipp" -version = "3.19.1" +version = "3.23.0" description = "Backport of pathlib-compatible object wrapper for zip files" optional = false -python-versions = ">=3.8" +python-versions = ">=3.9" groups = ["dev"] markers = "python_version < \"3.12\" and platform_machine != \"ppc64le\" and platform_machine != \"s390x\"" files = [ - {file = "zipp-3.19.1-py3-none-any.whl", hash = "sha256:2828e64edb5386ea6a52e7ba7cdb17bb30a73a858f5eb6eb93d8d36f5ea26091"}, - {file = "zipp-3.19.1.tar.gz", hash = "sha256:35427f6d5594f4acf82d25541438348c26736fa9b3afa2754bcd63cdb99d8e8f"}, + {file = "zipp-3.23.0-py3-none-any.whl", hash = "sha256:071652d6115ed432f5ce1d34c336c0adfd6a884660d1e9712a256d3d3bd4b14e"}, + {file = "zipp-3.23.0.tar.gz", hash = "sha256:a07157588a12518c9d4034df3fbbee09c814741a33ff63c05fa29d26a2404166"}, ] [package.extras] +check = ["pytest-checkdocs (>=2.4)", "pytest-ruff (>=0.2.1) ; sys_platform != \"cygwin\""] +cover = ["pytest-cov"] doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] -test = ["big-O", "jaraco.functools", "jaraco.itertools", "jaraco.test", "more-itertools", "pytest (>=6,!=8.1.*)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-ignore-flaky", "pytest-mypy", "pytest-ruff (>=0.2.1)"] +enabler = ["pytest-enabler (>=2.2)"] +test = ["big-O", "jaraco.functools", "jaraco.itertools", "jaraco.test", "more_itertools", "pytest (>=6,!=8.1.*)", "pytest-ignore-flaky"] +type = ["pytest-mypy"] [[package]] name = "zope-event" -version = "4.6" +version = "6.1" description = "Very basic event publishing system" optional = false -python-versions = "*" +python-versions = ">=3.10" groups = ["dev"] files = [ - {file = "zope.event-4.6-py2.py3-none-any.whl", hash = "sha256:73d9e3ef750cca14816a9c322c7250b0d7c9dbc337df5d1b807ff8d3d0b9e97c"}, - {file = "zope.event-4.6.tar.gz", hash = "sha256:81d98813046fc86cc4136e3698fee628a3282f9c320db18658c21749235fce80"}, + {file = "zope_event-6.1-py3-none-any.whl", hash = "sha256:0ca78b6391b694272b23ec1335c0294cc471065ed10f7f606858fc54566c25a0"}, + {file = "zope_event-6.1.tar.gz", hash = "sha256:6052a3e0cb8565d3d4ef1a3a7809336ac519bc4fe38398cb8d466db09adef4f0"}, ] -[package.dependencies] -setuptools = "*" - [package.extras] docs = ["Sphinx"] -test = ["zope.testrunner"] +test = ["zope.testrunner (>=6.4)"] [[package]] name = "zope-interface" -version = "8.0.1" +version = "8.2" description = "Interfaces for Python" optional = false -python-versions = ">=3.9" +python-versions = ">=3.10" groups = ["main", "dev"] files = [ - {file = "zope_interface-8.0.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:fd7195081b8637eeed8d73e4d183b07199a1dc738fb28b3de6666b1b55662570"}, - {file = "zope_interface-8.0.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:f7c4bc4021108847bce763673ce70d0716b08dfc2ba9889e7bad46ac2b3bb924"}, - {file = "zope_interface-8.0.1-cp310-cp310-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:758803806b962f32c87b31bb18c298b022965ba34fe532163831cc39118c24ab"}, - {file = "zope_interface-8.0.1-cp310-cp310-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:f8e88f35f86bbe8243cad4b2972deef0fdfca0a0723455abbebdc83bbab96b69"}, - {file = "zope_interface-8.0.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:7844765695937d9b0d83211220b72e2cf6ac81a08608ad2b58f2c094af498d83"}, - {file = "zope_interface-8.0.1-cp310-cp310-win_amd64.whl", hash = "sha256:64fa7b206dd9669f29d5c1241a768bebe8ab1e8a4b63ee16491f041e058c09d0"}, - {file = "zope_interface-8.0.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:4bd01022d2e1bce4a4a4ed9549edb25393c92e607d7daa6deff843f1f68b479d"}, - {file = "zope_interface-8.0.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:29be8db8b712d94f1c05e24ea230a879271d787205ba1c9a6100d1d81f06c69a"}, - {file = "zope_interface-8.0.1-cp311-cp311-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:51ae1b856565b30455b7879fdf0a56a88763b401d3f814fa9f9542d7410dbd7e"}, - {file = "zope_interface-8.0.1-cp311-cp311-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:d2e7596149cb1acd1d4d41b9f8fe2ffc0e9e29e2e91d026311814181d0d9efaf"}, - {file = "zope_interface-8.0.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:b2737c11c34fb9128816759864752d007ec4f987b571c934c30723ed881a7a4f"}, - {file = "zope_interface-8.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:cf66e4bf731aa7e0ced855bb3670e8cda772f6515a475c6a107bad5cb6604103"}, - {file = "zope_interface-8.0.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:115f27c1cc95ce7a517d960ef381beedb0a7ce9489645e80b9ab3cbf8a78799c"}, - {file = "zope_interface-8.0.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:af655c573b84e3cb6a4f6fd3fbe04e4dc91c63c6b6f99019b3713ef964e589bc"}, - {file = "zope_interface-8.0.1-cp312-cp312-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:23f82ef9b2d5370750cc1bf883c3b94c33d098ce08557922a3fbc7ff3b63dfe1"}, - {file = "zope_interface-8.0.1-cp312-cp312-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:35a1565d5244997f2e629c5c68715b3d9d9036e8df23c4068b08d9316dcb2822"}, - {file = "zope_interface-8.0.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:029ea1db7e855a475bf88d9910baab4e94d007a054810e9007ac037a91c67c6f"}, - {file = "zope_interface-8.0.1-cp312-cp312-win_amd64.whl", hash = "sha256:0beb3e7f7dc153944076fcaf717a935f68d39efa9fce96ec97bafcc0c2ea6cab"}, - {file = "zope_interface-8.0.1-cp313-cp313-macosx_10_9_x86_64.whl", hash = "sha256:c7cc027fc5c61c5d69e5080c30b66382f454f43dc379c463a38e78a9c6bab71a"}, - {file = "zope_interface-8.0.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:fcf9097ff3003b7662299f1c25145e15260ec2a27f9a9e69461a585d79ca8552"}, - {file = "zope_interface-8.0.1-cp313-cp313-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:6d965347dd1fb9e9a53aa852d4ded46b41ca670d517fd54e733a6b6a4d0561c2"}, - {file = "zope_interface-8.0.1-cp313-cp313-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:9a3b8bb77a4b89427a87d1e9eb969ab05e38e6b4a338a9de10f6df23c33ec3c2"}, - {file = "zope_interface-8.0.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:87e6b089002c43231fb9afec89268391bcc7a3b66e76e269ffde19a8112fb8d5"}, - {file = "zope_interface-8.0.1-cp313-cp313-win_amd64.whl", hash = "sha256:64a43f5280aa770cbafd0307cb3d1ff430e2a1001774e8ceb40787abe4bb6658"}, - {file = "zope_interface-8.0.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:b84464a9fcf801289fa8b15bfc0829e7855d47fb4a8059555effc6f2d1d9a613"}, - {file = "zope_interface-8.0.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:7b915cf7e747b5356d741be79a153aa9107e8923bc93bcd65fc873caf0fb5c50"}, - {file = "zope_interface-8.0.1-cp39-cp39-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:110c73ddf974b369ef3c6e7b0d87d44673cf4914eba3fe8a33bfb21c6c606ad8"}, - {file = "zope_interface-8.0.1-cp39-cp39-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:9e9bdca901c1bcc34e438001718512c65b3b8924aabcd732b6e7a7f0cd715f17"}, - {file = "zope_interface-8.0.1-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:bbd22d4801ad3e8ec704ba9e3e6a4ac2e875e4d77e363051ccb76153d24c5519"}, - {file = "zope_interface-8.0.1-cp39-cp39-win_amd64.whl", hash = "sha256:a0016ca85f93b938824e2f9a43534446e95134a2945b084944786e1ace2020bc"}, - {file = "zope_interface-8.0.1.tar.gz", hash = "sha256:eba5610d042c3704a48222f7f7c6ab5b243ed26f917e2bc69379456b115e02d1"}, + {file = "zope_interface-8.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:788c293f3165964ec6527b2d861072c68eef53425213f36d3893ebee89a89623"}, + {file = "zope_interface-8.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9a4e785097e741a1c953b3970ce28f2823bd63c00adc5d276f2981dd66c96c15"}, + {file = "zope_interface-8.2-cp310-cp310-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:16c69da19a06566664ddd4785f37cad5693a51d48df1515d264c20d005d322e2"}, + {file = "zope_interface-8.2-cp310-cp310-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:c31acfa3d7cde48bec45701b0e1f4698daffc378f559bfb296837d8c834732f6"}, + {file = "zope_interface-8.2-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:0723507127f8269b8f3f22663168f717e9c9742107d1b6c9f419df561b71aa6d"}, + {file = "zope_interface-8.2-cp310-cp310-win_amd64.whl", hash = "sha256:3bf73a910bb27344def2d301a03329c559a79b308e1e584686b74171d736be4e"}, + {file = "zope_interface-8.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c65ade7ea85516e428651048489f5e689e695c79188761de8c622594d1e13322"}, + {file = "zope_interface-8.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:a1ef4b43659e1348f35f38e7d1a6bbc1682efde239761f335ffc7e31e798b65b"}, + {file = "zope_interface-8.2-cp311-cp311-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:dfc4f44e8de2ff4eba20af4f0a3ca42d3c43ab24a08e49ccd8558b7a4185b466"}, + {file = "zope_interface-8.2-cp311-cp311-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:8f094bfb49179ec5dc9981cb769af1275702bd64720ef94874d9e34da1390d4c"}, + {file = "zope_interface-8.2-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d2bb8e7364e18f083bf6744ccf30433b2a5f236c39c95df8514e3c13007098ce"}, + {file = "zope_interface-8.2-cp311-cp311-win_amd64.whl", hash = "sha256:6f4b4dfcfdfaa9177a600bb31cebf711fdb8c8e9ed84f14c61c420c6aa398489"}, + {file = "zope_interface-8.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:624b6787fc7c3e45fa401984f6add2c736b70a7506518c3b537ffaacc4b29d4c"}, + {file = "zope_interface-8.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:bc9ded9e97a0ed17731d479596ed1071e53b18e6fdb2fc33af1e43f5fd2d3aaa"}, + {file = "zope_interface-8.2-cp312-cp312-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:532367553e4420c80c0fc0cabcc2c74080d495573706f66723edee6eae53361d"}, + {file = "zope_interface-8.2-cp312-cp312-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:2bf9cf275468bafa3c72688aad8cfcbe3d28ee792baf0b228a1b2d93bd1d541a"}, + {file = "zope_interface-8.2-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:0009d2d3c02ea783045d7804da4fd016245e5c5de31a86cebba66dd6914d59a2"}, + {file = "zope_interface-8.2-cp312-cp312-win_amd64.whl", hash = "sha256:845d14e580220ae4544bd4d7eb800f0b6034fe5585fc2536806e0a26c2ee6640"}, + {file = "zope_interface-8.2-cp313-cp313-macosx_10_9_x86_64.whl", hash = "sha256:6068322004a0158c80dfd4708dfb103a899635408c67c3b10e9acec4dbacefec"}, + {file = "zope_interface-8.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:2499de92e8275d0dd68f84425b3e19e9268cd1fa8507997900fa4175f157733c"}, + {file = "zope_interface-8.2-cp313-cp313-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:f777e68c76208503609c83ca021a6864902b646530a1a39abb9ed310d1100664"}, + {file = "zope_interface-8.2-cp313-cp313-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:9b05a919fdb0ed6ea942e5a7800e09a8b6cdae6f98fee1bef1c9d1a3fc43aaa0"}, + {file = "zope_interface-8.2-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:ccc62b5712dd7bd64cfba3ee63089fb11e840f5914b990033beeae3b2180b6cb"}, + {file = "zope_interface-8.2-cp313-cp313-win_amd64.whl", hash = "sha256:34f877d1d3bb7565c494ed93828fa6417641ca26faf6e8f044e0d0d500807028"}, + {file = "zope_interface-8.2-cp314-cp314-macosx_10_9_x86_64.whl", hash = "sha256:46c7e4e8cbc698398a67e56ca985d19cb92365b4aafbeb6a712e8c101090f4cb"}, + {file = "zope_interface-8.2-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:a87fc7517f825a97ff4a4ca4c8a950593c59e0f8e7bfe1b6f898a38d5ba9f9cf"}, + {file = "zope_interface-8.2-cp314-cp314-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:ccf52f7d44d669203c2096c1a0c2c15d52e36b2e7a9413df50f48392c7d4d080"}, + {file = "zope_interface-8.2-cp314-cp314-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:aae807efc7bd26302eb2fea05cd6de7d59269ed6ae23a6de1ee47add6de99b8c"}, + {file = "zope_interface-8.2-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:05a0e42d6d830f547e114de2e7cd15750dc6c0c78f8138e6c5035e51ddfff37c"}, + {file = "zope_interface-8.2-cp314-cp314-win_amd64.whl", hash = "sha256:561ce42390bee90bae51cf1c012902a8033b2aaefbd0deed81e877562a116d48"}, + {file = "zope_interface-8.2.tar.gz", hash = "sha256:afb20c371a601d261b4f6edb53c3c418c249db1a9717b0baafc9a9bb39ba1224"}, ] [package.extras] @@ -3515,24 +3732,23 @@ testing = ["coverage[toml]", "zope.event", "zope.testing"] [[package]] name = "zope-schema" -version = "7.0.1" +version = "8.1" description = "zope.interface extension for defining data schemas" optional = false -python-versions = ">=3.7" +python-versions = ">=3.9" groups = ["dev"] files = [ - {file = "zope.schema-7.0.1-py3-none-any.whl", hash = "sha256:cf006c678793b00e0075ad54d55281c8785ea21e5bc1f5ec0584787719c2aab2"}, - {file = "zope.schema-7.0.1.tar.gz", hash = "sha256:ead4dbcb03354d4e410c9a3b904451eb44d90254751b1cbdedf4a61aede9fbb9"}, + {file = "zope_schema-8.1-py3-none-any.whl", hash = "sha256:e5313ef0f20b0e5b6ecf9a75fd7bd59bc772244351144097e086a5822fe8b0bb"}, + {file = "zope_schema-8.1.tar.gz", hash = "sha256:2d9faa7f91a7bf09fcab2c039d9182fcc9ce7042dc77e18d3df277904aee0948"}, ] [package.dependencies] -setuptools = "*" "zope.event" = "*" "zope.interface" = ">=5.0.0" [package.extras] docs = ["Sphinx", "repoze.sphinx.autointerface"] -test = ["zope.i18nmessageid", "zope.testing", "zope.testrunner"] +test = ["zope.i18nmessageid", "zope.testing", "zope.testrunner (>=6.4)"] [extras] all = ["authlib", "defusedxml", "hiredis", "jaeger-client", "lxml", "matrix-synapse-ldap3", "opentracing", "psycopg2", "psycopg2cffi", "psycopg2cffi-compat", "pympler", "pysaml2", "pytz", "sentry-sdk", "thrift", "tornado", "txredisapi"] From c3af44339cdfa862515558729083b1c37f4194ea Mon Sep 17 00:00:00 2001 From: Eric Eastwood Date: Tue, 3 Mar 2026 08:13:59 -0600 Subject: [PATCH 047/121] Fix `/sync` missing membership in `state_after` (re-introduce) (#19460) *This PR was originally only to enable [MSC4222](https://github.com/matrix-org/matrix-spec-proposals/pull/4222) Complement tests (`/sync` `state_after`) but after merging the [fix PR](https://github.com/element-hq/synapse/pull/19463), we discovered that while the tests pass locally, [fail in CI](https://github.com/element-hq/synapse/pull/19460#discussion_r2818080879). To unblock the RC, we decided to revert the fix PR (see https://github.com/element-hq/synapse/pull/19474#discussion_r2818061001 for more info). To better ensure tests actually pass in CI, we're re-introducing the fix here in the same PR that we enable the tests in.* --- Fix `/sync` missing membership in `state_after`. This applies to any scenario where the first membership has a different `sender` compared to the `state_key` and then the second membership has the same `sender`/`state_key`. Like someone inviting another person and then them joining. Or someone being kicked and then they leave. This bug has been present since the MSC4222 implementation was introduced into the codebase (https://github.com/element-hq/synapse/pull/17888). --- Fix https://github.com/element-hq/synapse/issues/19455 Fix https://github.com/element-hq/customer-success/issues/656 I have a feeling, this might also fix these issues (will close and see how people report back): Fix https://github.com/element-hq/synapse/issues/18182 Fix https://github.com/element-hq/synapse/issues/19478 ### Testing strategy Complement tests: https://github.com/matrix-org/complement/pull/842 We will need https://github.com/element-hq/synapse/pull/19460 to merge in order to enable the Complement tests in Synapse but this PR should be merged first so they pass in the first place. I've tested locally that the Complement tests pass with this fix. ### Dev notes [MSC4222](https://github.com/matrix-org/matrix-spec-proposals/pull/4222) has already been merged into the spec and is already part of Matrix v1.16 but we haven't [stabilized support in Synapse yet](https://github.com/element-hq/synapse/issues/19414). --- In the same ballpark: - https://github.com/element-hq/synapse/issues/19455 - https://github.com/element-hq/synapse/issues/17050 - https://github.com/element-hq/synapse/issues/17430 - https://github.com/element-hq/synapse/issues/16940 - https://github.com/element-hq/synapse/issues/18182 - https://github.com/element-hq/synapse/issues/18793 - https://github.com/element-hq/synapse/issues/19478 --- Docker builds preferring remote image over the local image we just built, https://github.com/element-hq/synapse/pull/19460#discussion_r2818080879 `containerd` image store (storage driver, driver type) -> https://github.com/element-hq/synapse/pull/19475 ### Todo - [x] Wait for https://github.com/element-hq/synapse/pull/19463 to merge so the Complement tests all pass - [x] Wait for https://github.com/element-hq/synapse/pull/19475 to merge ### Pull Request Checklist * [x] Pull request is based on the develop branch * [x] Pull request includes a [changelog file](https://element-hq.github.io/synapse/latest/development/contributing_guide.html#changelog). The entry should: - Be a short description of your change which makes sense to users. "Fixed a bug that prevented receiving messages from other servers." instead of "Moved X method from `EventStore` to `EventWorkerStore`.". - Use markdown where necessary, mostly for `code blocks`. - End with either a period (.) or an exclamation mark (!). - Start with a capital letter. - Feel free to credit yourself, by adding a sentence "Contributed by @github_username." or "Contributed by [Your Name]." to the end of the entry. * [x] [Code style](https://element-hq.github.io/synapse/latest/code_style.html) is correct (run the [linters](https://element-hq.github.io/synapse/latest/development/contributing_guide.html#run-the-linters)) --------- Co-authored-by: Andrew Morgan <1342360+anoadragon453@users.noreply.github.com> Co-authored-by: Andrew Ferrazzutti --- changelog.d/19460.bugfix | 1 + .../complement/conf/workers-shared-extra.yaml.j2 | 2 ++ scripts-dev/complement.sh | 1 + synapse/handlers/sync.py | 15 ++++++++++++--- 4 files changed, 16 insertions(+), 3 deletions(-) create mode 100644 changelog.d/19460.bugfix diff --git a/changelog.d/19460.bugfix b/changelog.d/19460.bugfix new file mode 100644 index 00000000000..fe44c2ddf66 --- /dev/null +++ b/changelog.d/19460.bugfix @@ -0,0 +1 @@ +Fix `/sync` missing membership event in `state_after` (experimental [MSC4222](https://github.com/matrix-org/matrix-spec-proposals/pull/4222) implementation) in some scenarios. diff --git a/docker/complement/conf/workers-shared-extra.yaml.j2 b/docker/complement/conf/workers-shared-extra.yaml.j2 index 120b3b94962..9fd7fc954a3 100644 --- a/docker/complement/conf/workers-shared-extra.yaml.j2 +++ b/docker/complement/conf/workers-shared-extra.yaml.j2 @@ -141,6 +141,8 @@ experimental_features: msc4306_enabled: true # Sticky Events msc4354_enabled: true + # `/sync` `state_after` + msc4222_enabled: true server_notices: system_mxid_localpart: _server diff --git a/scripts-dev/complement.sh b/scripts-dev/complement.sh index 9a18a621ee9..fec005fdb13 100755 --- a/scripts-dev/complement.sh +++ b/scripts-dev/complement.sh @@ -264,6 +264,7 @@ main() { ./tests/msc4140 ./tests/msc4155 ./tests/msc4306 + ./tests/msc4222 ) # Export the list of test packages as a space-separated environment variable, so other diff --git a/synapse/handlers/sync.py b/synapse/handlers/sync.py index 2f405004de9..a32748c2a9c 100644 --- a/synapse/handlers/sync.py +++ b/synapse/handlers/sync.py @@ -1046,9 +1046,18 @@ async def compute_state_delta( if event.sender not in first_event_by_sender_map: first_event_by_sender_map[event.sender] = event - # We need the event's sender, unless their membership was in a - # previous timeline event. - if (EventTypes.Member, event.sender) not in timeline_state: + # When using `state_after`, there is no special treatment with + # regards to state also being in the `timeline`. Always fetch + # relevant membership regardless of whether the state event is in + # the `timeline`. + if sync_config.use_state_after: + members_to_fetch.add(event.sender) + # For `state`, the client is supposed to do a flawed re-construction + # of state over time by starting with the given `state` and layering + # on state from the `timeline` as you go (flawed because state + # resolution). In this case, we only need their membership in + # `state` when their membership isn't already in the `timeline`. + elif (EventTypes.Member, event.sender) not in timeline_state: members_to_fetch.add(event.sender) # FIXME: we also care about invite targets etc. From 160d9788c09e850b03115b78967f531fa695cd9f Mon Sep 17 00:00:00 2001 From: Eric Eastwood Date: Tue, 3 Mar 2026 08:24:25 -0600 Subject: [PATCH 048/121] Simplify Rust HTTP client response streaming and limiting (#19510) *As suggested by @sandhose in https://github.com/element-hq/synapse/pull/19498#discussion_r2865607737,* Simplify Rust HTTP client response streaming and limiting ### Dev notes Synapse's Rust HTTP client was introduced in https://github.com/element-hq/synapse/pull/18357 ### Pull Request Checklist * [x] Pull request is based on the develop branch * [x] Pull request includes a [changelog file](https://element-hq.github.io/synapse/latest/development/contributing_guide.html#changelog). The entry should: - Be a short description of your change which makes sense to users. "Fixed a bug that prevented receiving messages from other servers." instead of "Moved X method from `EventStore` to `EventWorkerStore`.". - Use markdown where necessary, mostly for `code blocks`. - End with either a period (.) or an exclamation mark (!). - Start with a capital letter. - Feel free to credit yourself, by adding a sentence "Contributed by @github_username." or "Contributed by [Your Name]." to the end of the entry. * [x] [Code style](https://element-hq.github.io/synapse/latest/code_style.html) is correct (run the [linters](https://element-hq.github.io/synapse/latest/development/contributing_guide.html#run-the-linters)) --- Cargo.lock | 1 + changelog.d/19510.misc | 1 + rust/Cargo.toml | 3 ++ rust/src/errors.rs | 2 +- rust/src/http_client.rs | 75 ++++++++------------------ tests/synapse_rust/test_http_client.py | 18 +++++++ 6 files changed, 45 insertions(+), 55 deletions(-) create mode 100644 changelog.d/19510.misc diff --git a/Cargo.lock b/Cargo.lock index c43ee29c76e..340114f8018 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -811,6 +811,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ab53c047fcd1a1d2a8820fe84f05d6be69e9526be40cb03b73f86b6b03e6d87d" dependencies = [ "anyhow", + "bytes", "indoc", "libc", "memoffset", diff --git a/changelog.d/19510.misc b/changelog.d/19510.misc new file mode 100644 index 00000000000..cafc26601f9 --- /dev/null +++ b/changelog.d/19510.misc @@ -0,0 +1 @@ +Simplify Rust HTTP client response streaming and limiting. diff --git a/rust/Cargo.toml b/rust/Cargo.toml index 350701d3279..8199a4e02ba 100644 --- a/rust/Cargo.toml +++ b/rust/Cargo.toml @@ -35,6 +35,9 @@ pyo3 = { version = "0.27.2", features = [ "anyhow", "abi3", "abi3-py310", + # So we can pass `bytes::Bytes` directly back to Python efficiently, + # https://docs.rs/pyo3/latest/pyo3/bytes/index.html + "bytes", ] } pyo3-log = "0.13.1" pythonize = "0.27.0" diff --git a/rust/src/errors.rs b/rust/src/errors.rs index 149019ff4b4..012f9a79904 100644 --- a/rust/src/errors.rs +++ b/rust/src/errors.rs @@ -62,7 +62,7 @@ impl NotFoundError { import_exception!(synapse.api.errors, HttpResponseException); impl HttpResponseException { - pub fn new(status: StatusCode, bytes: Vec) -> pyo3::PyErr { + pub fn new(status: StatusCode, bytes: bytes::Bytes) -> pyo3::PyErr { HttpResponseException::new_err(( status.as_u16(), status.canonical_reason().unwrap_or_default(), diff --git a/rust/src/http_client.rs b/rust/src/http_client.rs index dd37a10426b..398ba9041f2 100644 --- a/rust/src/http_client.rs +++ b/rust/src/http_client.rs @@ -15,8 +15,7 @@ use std::{collections::HashMap, future::Future, sync::OnceLock}; use anyhow::Context; -use futures::TryStreamExt; -use headers::HeaderMapExt; +use http_body_util::BodyExt; use once_cell::sync::OnceCell; use pyo3::{create_exception, exceptions::PyException, prelude::*}; use reqwest::RequestBuilder; @@ -236,62 +235,30 @@ impl HttpClient { let status = response.status(); - // Find the expected `Content-Length` so we can pre-allocate the buffer - // necessary to read the response. It's expected that not every request will - // have a `Content-Length` header. - // - // `response.content_length()` does exist but the "value does not directly - // represents the value of the `Content-Length` header, but rather the size - // of the response’s body" - // (https://docs.rs/reqwest/latest/reqwest/struct.Response.html#method.content_length) - // and we want to avoid reading the entire body at this point because we - // purposely stream it below until the `response_limit`. - let content_length = { - let content_length = response - .headers() - .typed_get::() - // We need a `usize` for the `Vec::with_capacity(...)` usage below - .and_then(|content_length| content_length.0.try_into().ok()); - - // Sanity check that the request isn't too large from the information - // they told us (may be inaccurate so we also check below as we actually - // read the bytes) - if let Some(content_length_bytes) = content_length { - if content_length_bytes > response_limit { - Err(anyhow::anyhow!( - "Response size (defined by `Content-Length`) too large" - ))?; - } - } - - content_length - }; - - // Stream the response to avoid allocating a giant object on the server - // above our expected `response_limit`. - let mut stream = response.bytes_stream(); - // Pre-allocate the buffer based on the expected `Content-Length` - let mut buffer = Vec::with_capacity( - content_length - // Default to pre-allocating nothing when the request doesn't have a - // `Content-Length` header - .unwrap_or(0), - ); - while let Some(chunk) = stream.try_next().await.context("reading body")? { - if buffer.len() + chunk.len() > response_limit { - Err(anyhow::anyhow!("Response size too large"))?; - } - - buffer.extend_from_slice(&chunk); - } + // A light-weight way to read the response up until the `response_limit`. We + // want to avoid allocating a giant response object on the server above our + // expected `response_limit` to avoid out-of-memory DOS problems. + let body = reqwest::Body::from(response); + let limited_body = http_body_util::Limited::new(body, response_limit); + let collected = limited_body + .collect() + .await + .map_err(anyhow::Error::from_boxed) + .with_context(|| { + format!( + "Response body exceeded response limit ({} bytes)", + response_limit + ) + })?; + let bytes: bytes::Bytes = collected.to_bytes(); if !status.is_success() { - return Err(HttpResponseException::new(status, buffer)); + return Err(HttpResponseException::new(status, bytes)); } - let r = Python::attach(|py| buffer.into_pyobject(py).map(|o| o.unbind()))?; - - Ok(r) + // Because of the `pyo3` `bytes` feature, we can pass this back to Python + // land efficiently + Ok(bytes) }) } } diff --git a/tests/synapse_rust/test_http_client.py b/tests/synapse_rust/test_http_client.py index 032eab77e80..56fab3a0e1d 100644 --- a/tests/synapse_rust/test_http_client.py +++ b/tests/synapse_rust/test_http_client.py @@ -171,6 +171,24 @@ async def do_request() -> None: self.get_success(self.till_deferred_has_result(do_request())) self.assertEqual(self.server.calls, 1) + def test_request_response_limit_exceeded(self) -> None: + """ + Test to make sure we handle the response limit being exceeded + """ + + async def do_request() -> None: + await self._rust_http_client.get( + url=self.server.endpoint, + # Small limit so we hit the limit + response_limit=1, + ) + + self.assertFailure( + self.till_deferred_has_result(do_request()), + RuntimeError, + ) + self.assertEqual(self.server.calls, 1) + async def test_logging_context(self) -> None: """ Test to make sure the `LoggingContext` (logcontext) is handled correctly From 639922e8359b7f67f236eed21e5c13aa16428f14 Mon Sep 17 00:00:00 2001 From: Quentin Gliech Date: Tue, 3 Mar 2026 15:38:17 +0100 Subject: [PATCH 049/121] 1.149.0rc1 --- CHANGES.md | 30 ++++++++++++++++++++++++++++++ changelog.d/18518.bugfix | 1 - changelog.d/19127.feature | 1 - changelog.d/19431.feature | 1 - changelog.d/19440.misc | 1 - changelog.d/19460.bugfix | 1 - changelog.d/19461.misc | 1 - changelog.d/19475.misc | 1 - changelog.d/19480.misc | 1 - changelog.d/19486.misc | 1 - changelog.d/19496.misc | 1 - changelog.d/19498.misc | 1 - changelog.d/19499.misc | 1 - changelog.d/19507.misc | 1 - changelog.d/19510.misc | 1 - changelog.d/19515.misc | 1 - changelog.d/19519.misc | 1 - debian/changelog | 6 ++++++ pyproject.toml | 2 +- schema/synapse-config.schema.yaml | 2 +- 20 files changed, 38 insertions(+), 18 deletions(-) delete mode 100644 changelog.d/18518.bugfix delete mode 100644 changelog.d/19127.feature delete mode 100644 changelog.d/19431.feature delete mode 100644 changelog.d/19440.misc delete mode 100644 changelog.d/19460.bugfix delete mode 100644 changelog.d/19461.misc delete mode 100644 changelog.d/19475.misc delete mode 100644 changelog.d/19480.misc delete mode 100644 changelog.d/19486.misc delete mode 100644 changelog.d/19496.misc delete mode 100644 changelog.d/19498.misc delete mode 100644 changelog.d/19499.misc delete mode 100644 changelog.d/19507.misc delete mode 100644 changelog.d/19510.misc delete mode 100644 changelog.d/19515.misc delete mode 100644 changelog.d/19519.misc diff --git a/CHANGES.md b/CHANGES.md index cae604adac6..5996b7bcdfd 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,3 +1,33 @@ +# Synapse 1.149.0rc1 (2026-03-03) + +## Features + +- Add experimental support for [MSC4388: Secure out-of-band channel for sign in with QR](https://github.com/matrix-org/matrix-spec-proposals/pull/4388). ([\#19127](https://github.com/element-hq/synapse/issues/19127)) +- Add stable support for [MSC4380](https://github.com/matrix-org/matrix-spec-proposals/pull/4380) invite blocking. ([\#19431](https://github.com/element-hq/synapse/issues/19431)) + +## Bugfixes + +- Fix the 'Login as a user' Admin API not checking if the user exists before issuing an access token. ([\#18518](https://github.com/element-hq/synapse/issues/18518)) +- Fix `/sync` missing membership event in `state_after` (experimental [MSC4222](https://github.com/matrix-org/matrix-spec-proposals/pull/4222) implementation) in some scenarios. ([\#19460](https://github.com/element-hq/synapse/issues/19460)) + +## Internal Changes + +- Add log to explain when and why we freeze objects in the garbage collector. ([\#19440](https://github.com/element-hq/synapse/issues/19440)) +- Better instrument `JoinRoomAliasServlet` with tracing. ([\#19461](https://github.com/element-hq/synapse/issues/19461)) +- Fix Complement CI not running against the code from our PRs. ([\#19475](https://github.com/element-hq/synapse/issues/19475)) +- Log `docker system info` in CI so we have a plain record of how GitHub runners evolve over time. ([\#19480](https://github.com/element-hq/synapse/issues/19480)) +- Rename the `test_disconnect` test helper so that pytest doesn't see it as a test. ([\#19486](https://github.com/element-hq/synapse/issues/19486)) +- Add a log line when we delete devices. Contributed by @bradtgmurray @ Beeper. ([\#19496](https://github.com/element-hq/synapse/issues/19496)) +- Pre-allocate the buffer based on the expected `Content-Length` with the Rust HTTP client. ([\#19498](https://github.com/element-hq/synapse/issues/19498)) +- Cancel long-running sync requests if the client has gone away. ([\#19499](https://github.com/element-hq/synapse/issues/19499)) +- Try and reduce reactor tick times when under heavy load. ([\#19507](https://github.com/element-hq/synapse/issues/19507)) +- Simplify Rust HTTP client response streaming and limiting. ([\#19510](https://github.com/element-hq/synapse/issues/19510)) +- Replace deprecated collection import locations with current locations. ([\#19515](https://github.com/element-hq/synapse/issues/19515)) +- Bump most locked Python dependencies to their latest versions. ([\#19519](https://github.com/element-hq/synapse/issues/19519)) + + + + # Synapse 1.148.0 (2026-02-24) No significant changes since 1.148.0rc1. diff --git a/changelog.d/18518.bugfix b/changelog.d/18518.bugfix deleted file mode 100644 index 959528d7c84..00000000000 --- a/changelog.d/18518.bugfix +++ /dev/null @@ -1 +0,0 @@ -Fix the 'Login as a user' Admin API not checking if the user exists before issuing an access token. \ No newline at end of file diff --git a/changelog.d/19127.feature b/changelog.d/19127.feature deleted file mode 100644 index 7dc3a49f368..00000000000 --- a/changelog.d/19127.feature +++ /dev/null @@ -1 +0,0 @@ -Add experimental support for [MSC4388: Secure out-of-band channel for sign in with QR](https://github.com/matrix-org/matrix-spec-proposals/pull/4388). diff --git a/changelog.d/19431.feature b/changelog.d/19431.feature deleted file mode 100644 index 0076e5221c6..00000000000 --- a/changelog.d/19431.feature +++ /dev/null @@ -1 +0,0 @@ -Add stable support for [MSC4380](https://github.com/matrix-org/matrix-spec-proposals/pull/4380) invite blocking. diff --git a/changelog.d/19440.misc b/changelog.d/19440.misc deleted file mode 100644 index 77776011144..00000000000 --- a/changelog.d/19440.misc +++ /dev/null @@ -1 +0,0 @@ -Add log to explain when and why we freeze objects in the garbage collector. diff --git a/changelog.d/19460.bugfix b/changelog.d/19460.bugfix deleted file mode 100644 index fe44c2ddf66..00000000000 --- a/changelog.d/19460.bugfix +++ /dev/null @@ -1 +0,0 @@ -Fix `/sync` missing membership event in `state_after` (experimental [MSC4222](https://github.com/matrix-org/matrix-spec-proposals/pull/4222) implementation) in some scenarios. diff --git a/changelog.d/19461.misc b/changelog.d/19461.misc deleted file mode 100644 index 9c444ffe3c9..00000000000 --- a/changelog.d/19461.misc +++ /dev/null @@ -1 +0,0 @@ -Better instrument `JoinRoomAliasServlet` with tracing. diff --git a/changelog.d/19475.misc b/changelog.d/19475.misc deleted file mode 100644 index ea2774dcb15..00000000000 --- a/changelog.d/19475.misc +++ /dev/null @@ -1 +0,0 @@ -Fix Complement CI not running against the code from our PRs. diff --git a/changelog.d/19480.misc b/changelog.d/19480.misc deleted file mode 100644 index 60abe04962c..00000000000 --- a/changelog.d/19480.misc +++ /dev/null @@ -1 +0,0 @@ -Log `docker system info` in CI so we have a plain record of how GitHub runners evolve over time. diff --git a/changelog.d/19486.misc b/changelog.d/19486.misc deleted file mode 100644 index c0c2fef770c..00000000000 --- a/changelog.d/19486.misc +++ /dev/null @@ -1 +0,0 @@ -Rename the `test_disconnect` test helper so that pytest doesn't see it as a test. \ No newline at end of file diff --git a/changelog.d/19496.misc b/changelog.d/19496.misc deleted file mode 100644 index 83c5d6987c0..00000000000 --- a/changelog.d/19496.misc +++ /dev/null @@ -1 +0,0 @@ -Add a log line when we delete devices. Contributed by @bradtgmurray @ Beeper. diff --git a/changelog.d/19498.misc b/changelog.d/19498.misc deleted file mode 100644 index 7d048c283d6..00000000000 --- a/changelog.d/19498.misc +++ /dev/null @@ -1 +0,0 @@ -Pre-allocate the buffer based on the expected `Content-Length` with the Rust HTTP client. diff --git a/changelog.d/19499.misc b/changelog.d/19499.misc deleted file mode 100644 index c2641c1e58d..00000000000 --- a/changelog.d/19499.misc +++ /dev/null @@ -1 +0,0 @@ -Cancel long-running sync requests if the client has gone away. diff --git a/changelog.d/19507.misc b/changelog.d/19507.misc deleted file mode 100644 index 5a7ccff988c..00000000000 --- a/changelog.d/19507.misc +++ /dev/null @@ -1 +0,0 @@ -Try and reduce reactor tick times when under heavy load. diff --git a/changelog.d/19510.misc b/changelog.d/19510.misc deleted file mode 100644 index cafc26601f9..00000000000 --- a/changelog.d/19510.misc +++ /dev/null @@ -1 +0,0 @@ -Simplify Rust HTTP client response streaming and limiting. diff --git a/changelog.d/19515.misc b/changelog.d/19515.misc deleted file mode 100644 index d33af364066..00000000000 --- a/changelog.d/19515.misc +++ /dev/null @@ -1 +0,0 @@ -Replace deprecated collection import locations with current locations. \ No newline at end of file diff --git a/changelog.d/19519.misc b/changelog.d/19519.misc deleted file mode 100644 index 860a5f5130d..00000000000 --- a/changelog.d/19519.misc +++ /dev/null @@ -1 +0,0 @@ -Bump most locked Python dependencies to their latest versions. diff --git a/debian/changelog b/debian/changelog index 31fd948be1e..583bb35ad5b 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,3 +1,9 @@ +matrix-synapse-py3 (1.149.0~rc1) stable; urgency=medium + + * New synapse release 1.149.0rc1. + + -- Synapse Packaging team Tue, 03 Mar 2026 14:37:57 +0000 + matrix-synapse-py3 (1.148.0) stable; urgency=medium * New synapse release 1.148.0. diff --git a/pyproject.toml b/pyproject.toml index 2d55d3c7a59..9d5e3fec2d6 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "matrix-synapse" -version = "1.148.0" +version = "1.149.0rc1" description = "Homeserver for the Matrix decentralised comms protocol" readme = "README.rst" authors = [ diff --git a/schema/synapse-config.schema.yaml b/schema/synapse-config.schema.yaml index 42e9ed87dda..2ded5c12bad 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.148/synapse-config.schema.json +$id: https://element-hq.github.io/synapse/schema/synapse/v1.149/synapse-config.schema.json type: object properties: modules: From 51048b8e36e926a9151f1488ed0aca88d305b4d8 Mon Sep 17 00:00:00 2001 From: Eric Eastwood Date: Tue, 3 Mar 2026 10:04:37 -0600 Subject: [PATCH 050/121] Update docs to clarify `outbound_federation_restricted_to` can also be used with the SBG (#19517) [Secure Border Gateway (SBG)](https://element.io/en/server-suite/secure-border-gateways) Spawning from [internal discussion](https://matrix.to/#/!mNoPShRlwEeyHAEJOe:element.io/$6eGip85OUKOmyK1VzqrFMc7eF7dON7Vs76O40kVbRRY?via=banzan.uk&via=element.io&via=jki.re) around integrating [Synapse Pro for small hosts](https://docs.element.io/latest/element-server-suite-pro/synapse-pro-for-small-hosts/overview) in the [Element Server Suite (ESS)](https://element.io/en/server-suite) stack and wanting it be compatible with the SBG. We know that the SBG works with monolith Synapse because that's what we have configured with the [Complement tests in the SBG repo](https://github.com/element-hq/sbg/blob/b76b05b53e40bf6890e51dd1b83cec3460274eb2/complement/configure_synapse_for_sbg.sh#L8-L10). --- changelog.d/19517.doc | 1 + docs/usage/configuration/config_documentation.md | 2 +- schema/synapse-config.schema.yaml | 12 +++++++----- 3 files changed, 9 insertions(+), 6 deletions(-) create mode 100644 changelog.d/19517.doc diff --git a/changelog.d/19517.doc b/changelog.d/19517.doc new file mode 100644 index 00000000000..778c14c6aac --- /dev/null +++ b/changelog.d/19517.doc @@ -0,0 +1 @@ +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/docs/usage/configuration/config_documentation.md b/docs/usage/configuration/config_documentation.md index 84fd3dad54d..1335def5857 100644 --- a/docs/usage/configuration/config_documentation.md +++ b/docs/usage/configuration/config_documentation.md @@ -4484,7 +4484,7 @@ stream_writers: --- ### `outbound_federation_restricted_to` -*(array)* When using workers, you can restrict outbound federation traffic to only go through a specific subset of workers. Any worker specified here must also be in the [`instance_map`](#instance_map). [`worker_replication_secret`](#worker_replication_secret) must also be configured to authorize inter-worker communication. +*(array)* You can restrict outbound federation traffic to only go through a specific subset of workers including the [Secure Border Gateway (SBG)](https://element.io/en/server-suite/secure-border-gateways). Any worker specified here (including the SBG) must also be in the [`instance_map`](#instance_map). [`worker_replication_secret`](#worker_replication_secret) must also be configured to authorize inter-worker communication. Also see the [worker documentation](../../workers.md#restrict-outbound-federation-traffic-to-a-specific-set-of-workers) for more info. diff --git a/schema/synapse-config.schema.yaml b/schema/synapse-config.schema.yaml index 42e9ed87dda..31cdb101e24 100644 --- a/schema/synapse-config.schema.yaml +++ b/schema/synapse-config.schema.yaml @@ -5529,11 +5529,13 @@ properties: outbound_federation_restricted_to: type: array description: >- - When using workers, you can restrict outbound federation traffic to only - go through a specific subset of workers. Any worker specified here must - also be in the [`instance_map`](#instance_map). - [`worker_replication_secret`](#worker_replication_secret) must also be - configured to authorize inter-worker communication. + You can restrict outbound federation traffic to only go through a specific subset + of workers including the [Secure Border Gateway + (SBG)](https://element.io/en/server-suite/secure-border-gateways). Any worker + specified here (including the SBG) must also be in the + [`instance_map`](#instance_map). + [`worker_replication_secret`](#worker_replication_secret) must also be configured + to authorize inter-worker communication. Also see the [worker From c2c05879bbae8eca05fbe21a6040bb27298549ee Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 3 Mar 2026 17:30:30 +0000 Subject: [PATCH 051/121] Bump docker/build-push-action from 6.18.0 to 6.19.2 in the minor-and-patches group (#19514) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/docker.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml index 6fb6b81deb9..b1dd8a75e77 100644 --- a/.github/workflows/docker.yml +++ b/.github/workflows/docker.yml @@ -87,7 +87,7 @@ jobs: - name: Build and push by digest id: build - uses: docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83 # v6.18.0 + uses: docker/build-push-action@10e90e3645eae34f1e60eeb005ba3a3d33f178e8 # v6.19.2 with: push: true labels: | From 46c6e0ae1e3fe4abf7ef558e6d6bd5455a143d8a Mon Sep 17 00:00:00 2001 From: Eric Eastwood Date: Tue, 3 Mar 2026 13:18:49 -0600 Subject: [PATCH 052/121] Unify Complement developer docs (#19518) Instead of having info spread across a few places, consolidate and link to one spot. --- changelog.d/19518.doc | 1 + complement/README.md | 49 ++++++++++++++++++++++-- docker/README-testing.md | 4 +- docs/development/contributing_guide.md | 53 +------------------------- 4 files changed, 49 insertions(+), 58 deletions(-) create mode 100644 changelog.d/19518.doc diff --git a/changelog.d/19518.doc b/changelog.d/19518.doc new file mode 100644 index 00000000000..5de867c8d7b --- /dev/null +++ b/changelog.d/19518.doc @@ -0,0 +1 @@ +Unify Complement developer docs. diff --git a/complement/README.md b/complement/README.md index c4a95bdd63a..77f7ee182da 100644 --- a/complement/README.md +++ b/complement/README.md @@ -8,8 +8,7 @@ ensure everything works at a holistic level. ## Setup Nothing beyond a [normal Complement -setup](https://github.com/matrix-org/complement?tab=readme-ov-file#running) (just Go and -Docker). +setup](https://github.com/matrix-org/complement#running) (just Go and Docker). ## Running tests @@ -28,14 +27,39 @@ scripts-dev/complement.sh ./tests/csapi/... -run TestRoomCreate/Parallel/POST_/c scripts-dev/complement.sh ./tests/... -run 'TestRoomCreate/Parallel/POST_/createRoom_makes_a_(.*)' ``` -Typically, if you're developing the Synapse and Complement tests side-by-side, you will -run something like this: +It's often nice to develop on Synapse and write Complement tests at the same time. +Here is how to run your local Synapse checkout against your local Complement checkout. ```shell # To run a specific test COMPLEMENT_DIR=../complement ./scripts-dev/complement.sh ./tests/csapi/... -run TestRoomCreate ``` +The above will run a monolithic (single-process) Synapse with SQLite as the database. +For other configurations, try: + +- Passing `POSTGRES=1` as an environment variable to use the Postgres database instead. +- Passing `WORKERS=1` as an environment variable to use a workerised setup instead. This + option implies the use of Postgres. + - If setting `WORKERS=1`, optionally set `WORKER_TYPES=` to declare which worker types + you wish to test. A simple comma-delimited string containing the worker types + defined from the `WORKERS_CONFIG` template in + [here](https://github.com/element-hq/synapse/blob/develop/docker/configure_workers_and_start.py#L54). + A safe example would be `WORKER_TYPES="federation_inbound, federation_sender, + synchrotron"`. See the [worker documentation](../workers.md) for additional + information on workers. +- Passing `ASYNCIO_REACTOR=1` as an environment variable to use the asyncio-backed + reactor with Twisted instead of the default one. +- Passing `PODMAN=1` will use the [podman](https://podman.io/) container runtime, + instead of docker. +- Passing `UNIX_SOCKETS=1` will utilise Unix socket functionality for Synapse, Redis, + and Postgres(when applicable). + +To increase the log level for the tests, set `SYNAPSE_TEST_LOG_LEVEL`, e.g: +```sh +SYNAPSE_TEST_LOG_LEVEL=DEBUG COMPLEMENT_DIR=../complement ./scripts-dev/complement.sh -run TestRoomCreate +``` + ### Running in-repo tests @@ -52,3 +76,20 @@ To run the in-repo Complement tests, use the `--in-repo` command line argument. # Similarly, you can also use `-run` to specify all or part of a specific test path to run scripts-dev/complement.sh --in-repo ./tests/... -run TestIntraShardFederation ``` + +### Access database for homeserver after Complement test runs. + +If you're curious what the database looks like after you run some tests, here are some +steps to get you going in Synapse: + +1. In your Complement test comment out `defer deployment.Destroy(t)` and replace with + `defer time.Sleep(2 * time.Hour)` to keep the homeserver running after the tests + complete +1. Start the Complement tests +1. Find the name of the container, `docker ps -f name=complement_` (this will filter for + just the Complement related Docker containers) +1. Access the container replacing the name with what you found in the previous step: + `docker exec -it complement_1_hs_with_application_service.hs1_2 /bin/bash` +1. Install sqlite (database driver), `apt-get update && apt-get install -y sqlite3` +1. Then run `sqlite3` and open the database `.open /conf/homeserver.db` (this db path + comes from the Synapse homeserver.yaml) diff --git a/docker/README-testing.md b/docker/README-testing.md index aeeba58730a..8ce7dffc336 100644 --- a/docker/README-testing.md +++ b/docker/README-testing.md @@ -12,11 +12,9 @@ Note that running Synapse's unit tests from within the docker image is not suppo `scripts-dev/complement.sh` is a script that will automatically build and run Synapse against Complement. -Consult the [contributing guide][guideComplementSh] for instructions on how to use it. +Consult our [Complement docs][https://github.com/element-hq/synapse/tree/develop/complement] for instructions on how to use it. -[guideComplementSh]: https://element-hq.github.io/synapse/latest/development/contributing_guide.html#run-the-integration-tests-complement - ## Building and running the images manually Under some circumstances, you may wish to build the images manually. diff --git a/docs/development/contributing_guide.md b/docs/development/contributing_guide.md index 41fff1d6a37..ab506b5f05a 100644 --- a/docs/development/contributing_guide.md +++ b/docs/development/contributing_guide.md @@ -334,46 +334,9 @@ For more details about other configurations, see the [Docker-specific documentat ## Run the integration tests ([Complement](https://github.com/matrix-org/complement)). -[Complement](https://github.com/matrix-org/complement) is a suite of black box tests that can be run on any homeserver implementation. It can also be thought of as end-to-end (e2e) tests. +See our [Complement docs](https://github.com/element-hq/synapse/tree/develop/complement) +for how to use the `./scripts-dev/complement.sh` test runner script. -It's often nice to develop on Synapse and write Complement tests at the same time. -Here is how to run your local Synapse checkout against your local Complement checkout. - -(checkout [`complement`](https://github.com/matrix-org/complement) alongside your `synapse` checkout) -```sh -COMPLEMENT_DIR=../complement ./scripts-dev/complement.sh -``` - -To run a specific test file, you can pass the test name at the end of the command. The name passed comes from the naming structure in your Complement tests. If you're unsure of the name, you can do a full run and copy it from the test output: - -```sh -COMPLEMENT_DIR=../complement ./scripts-dev/complement.sh -run TestImportHistoricalMessages -``` - -To run a specific test, you can specify the whole name structure: - -```sh -COMPLEMENT_DIR=../complement ./scripts-dev/complement.sh -run TestImportHistoricalMessages/parallel/Historical_events_resolve_in_the_correct_order -``` - -The above will run a monolithic (single-process) Synapse with SQLite as the database. For other configurations, try: - -- Passing `POSTGRES=1` as an environment variable to use the Postgres database instead. -- Passing `WORKERS=1` as an environment variable to use a workerised setup instead. This option implies the use of Postgres. - - If setting `WORKERS=1`, optionally set `WORKER_TYPES=` to declare which worker - types you wish to test. A simple comma-delimited string containing the worker types - defined from the `WORKERS_CONFIG` template in - [here](https://github.com/element-hq/synapse/blob/develop/docker/configure_workers_and_start.py#L54). - A safe example would be `WORKER_TYPES="federation_inbound, federation_sender, synchrotron"`. - See the [worker documentation](../workers.md) for additional information on workers. -- Passing `ASYNCIO_REACTOR=1` as an environment variable to use the Twisted asyncio reactor instead of the default one. -- Passing `PODMAN=1` will use the [podman](https://podman.io/) container runtime, instead of docker. -- Passing `UNIX_SOCKETS=1` will utilise Unix socket functionality for Synapse, Redis, and Postgres(when applicable). - -To increase the log level for the tests, set `SYNAPSE_TEST_LOG_LEVEL`, e.g: -```sh -SYNAPSE_TEST_LOG_LEVEL=DEBUG COMPLEMENT_DIR=../complement ./scripts-dev/complement.sh -run TestImportHistoricalMessages -``` ### Prettier formatting with `gotestfmt` @@ -389,18 +352,6 @@ COMPLEMENT_DIR=../complement ./scripts-dev/complement.sh -json | gotestfmt -hide (Remove `-hide successful-tests` if you don't want to hide successful tests.) -### Access database for homeserver after Complement test runs. - -If you're curious what the database looks like after you run some tests, here are some steps to get you going in Synapse: - -1. In your Complement test comment out `defer deployment.Destroy(t)` and replace with `defer time.Sleep(2 * time.Hour)` to keep the homeserver running after the tests complete -1. Start the Complement tests -1. Find the name of the container, `docker ps -f name=complement_` (this will filter for just the Compelement related Docker containers) -1. Access the container replacing the name with what you found in the previous step: `docker exec -it complement_1_hs_with_application_service.hs1_2 /bin/bash` -1. Install sqlite (database driver), `apt-get update && apt-get install -y sqlite3` -1. Then run `sqlite3` and open the database `.open /conf/homeserver.db` (this db path comes from the Synapse homeserver.yaml) - - # 9. Submit your patch. Once you're happy with your patch, it's time to prepare a Pull Request. From 699a898b30954bd610ad7919888cde03125d4d5f Mon Sep 17 00:00:00 2001 From: Mathieu Velten Date: Thu, 5 Mar 2026 15:46:05 +0100 Subject: [PATCH 053/121] Backgrounds membership updates when changing the avatar or the display name (#19311) --- changelog.d/19311.misc | 1 + synapse/handlers/profile.py | 77 ++++++++++++- synapse/util/task_scheduler.py | 5 +- tests/handlers/test_profile.py | 205 ++++++++++++++++++++++++++++++++- 4 files changed, 283 insertions(+), 5 deletions(-) create mode 100644 changelog.d/19311.misc diff --git a/changelog.d/19311.misc b/changelog.d/19311.misc new file mode 100644 index 00000000000..66ec86c02d0 --- /dev/null +++ b/changelog.d/19311.misc @@ -0,0 +1 @@ +Put membership updates in a background resumable task when changing the avatar or the display name. diff --git a/synapse/handlers/profile.py b/synapse/handlers/profile.py index 8f16ae6dece..6bec901a792 100644 --- a/synapse/handlers/profile.py +++ b/synapse/handlers/profile.py @@ -20,8 +20,11 @@ # import logging import random +from bisect import bisect_right from typing import TYPE_CHECKING +from twisted.internet.defer import CancelledError + from synapse.api.constants import ProfileFields from synapse.api.errors import ( AuthError, @@ -32,7 +35,16 @@ SynapseError, ) from synapse.storage.databases.main.media_repository import LocalMedia, RemoteMedia -from synapse.types import JsonDict, JsonValue, Requester, UserID, create_requester +from synapse.types import ( + JsonDict, + JsonMapping, + JsonValue, + Requester, + ScheduledTask, + TaskStatus, + UserID, + create_requester, +) from synapse.util.caches.descriptors import cached from synapse.util.duration import Duration from synapse.util.stringutils import parse_and_validate_mxc_uri @@ -46,6 +58,8 @@ MAX_AVATAR_URL_LEN = 1000 # Field name length is specced at 255 bytes. MAX_CUSTOM_FIELD_LEN = 255 +UPDATE_JOIN_STATES_ACTION_NAME = "update_join_states" +UPDATE_JOIN_STATES_LOCK_NAME = "update_join_states_lock" class ProfileHandler: @@ -78,6 +92,12 @@ def __init__(self, hs: "HomeServer"): self._third_party_rules = hs.get_module_api_callbacks().third_party_event_rules + self._task_scheduler = hs.get_task_scheduler() + self._task_scheduler.register_action( + self._update_join_states_task, UPDATE_JOIN_STATES_ACTION_NAME + ) + self._worker_locks = hs.get_worker_locks_handler() + async def get_profile(self, user_id: str, ignore_backoff: bool = True) -> JsonDict: """ Get a user's profile as a JSON dictionary. @@ -587,7 +607,53 @@ async def _update_join_states( await self.clock.sleep(Duration(seconds=random.randint(1, 10))) return - room_ids = await self.store.get_rooms_for_user(target_user.to_string()) + target_user_str = target_user.to_string() + + # Cancel any ongoing profile membership updates for this user, + # and start a new one. + async with self._worker_locks.acquire_lock( + UPDATE_JOIN_STATES_LOCK_NAME, + target_user_str, + ): + tasks_to_cancel = await self._task_scheduler.get_tasks( + actions=[UPDATE_JOIN_STATES_ACTION_NAME], + resource_id=target_user_str, + statuses=[TaskStatus.ACTIVE, TaskStatus.SCHEDULED], + ) + assert len(tasks_to_cancel) <= 1, "Expected at most one task to cancel" + for task in tasks_to_cancel: + await self._task_scheduler.cancel_task(task.id) + + await self._task_scheduler.schedule_task( + UPDATE_JOIN_STATES_ACTION_NAME, + resource_id=target_user_str, + params={ + "requester_authenticated_entity": requester.authenticated_entity, + }, + ) + + async def _update_join_states_task( + self, + task: ScheduledTask, + ) -> tuple[TaskStatus, JsonMapping | None, str | None]: + assert task.resource_id + assert task.params + + target_user = UserID.from_string(task.resource_id) + room_ids = sorted(await self.store.get_rooms_for_user(target_user.to_string())) + + last_room_id = task.result.get("last_room_id", None) if task.result else None + + if last_room_id: + # Filter out room IDs that have already been handled + # by finding the first room ID greater than the last handled room ID + # and slicing the list from that point onwards. + room_ids = room_ids[bisect_right(room_ids, last_room_id) :] + + requester = create_requester( + user_id=target_user, + authenticated_entity=task.params.get("requester_authenticated_entity"), + ) for room_id in room_ids: handler = self.hs.get_room_member_handler() @@ -601,10 +667,17 @@ async def _update_join_states( "join", # We treat a profile update like a join. ratelimit=False, # Try to hide that these events aren't atomic. ) + except CancelledError as e: + raise e except Exception as e: logger.warning( "Failed to update join event for room %s - %s", room_id, str(e) ) + await self._task_scheduler.update_task( + task.id, result={"last_room_id": room_id} + ) + + return TaskStatus.COMPLETE, None, None async def check_profile_query_allowed( self, target_user: UserID, requester: UserID | None = None diff --git a/synapse/util/task_scheduler.py b/synapse/util/task_scheduler.py index e5cfc85a37d..c1790fd3ae4 100644 --- a/synapse/util/task_scheduler.py +++ b/synapse/util/task_scheduler.py @@ -471,8 +471,12 @@ async def wrapper() -> None: log_context, start_time, ) + result = None + error = None try: (status, result, error) = await function(task) + except defer.CancelledError: + status = TaskStatus.CANCELLED except Exception: f = Failure() logger.error( @@ -481,7 +485,6 @@ async def wrapper() -> None: exc_info=(f.type, f.value, f.getTracebackObject()), ) status = TaskStatus.FAILED - result = None error = f.getErrorMessage() await self._store.update_scheduled_task( diff --git a/tests/handlers/test_profile.py b/tests/handlers/test_profile.py index 7a7f803ebd5..2e521a86b60 100644 --- a/tests/handlers/test_profile.py +++ b/tests/handlers/test_profile.py @@ -19,18 +19,23 @@ # # from typing import Any, Awaitable, Callable -from unittest.mock import AsyncMock, Mock +from unittest.mock import AsyncMock, Mock, patch from parameterized import parameterized from twisted.internet.testing import MemoryReactor import synapse.types +from synapse.api.constants import EventTypes from synapse.api.errors import AuthError, SynapseError from synapse.rest import admin +from synapse.rest.client import login, room from synapse.server import HomeServer from synapse.types import JsonDict, UserID +from synapse.types.state import StateFilter from synapse.util.clock import Clock +from synapse.util.duration import Duration +from synapse.util.task_scheduler import TaskStatus from tests import unittest @@ -38,7 +43,11 @@ class ProfileTestCase(unittest.HomeserverTestCase): """Tests profile management.""" - servlets = [admin.register_servlets] + servlets = [ + admin.register_servlets, + login.register_servlets, + room.register_servlets, + ] def make_homeserver(self, reactor: MemoryReactor, clock: Clock) -> HomeServer: self.mock_federation = AsyncMock() @@ -62,12 +71,15 @@ def register_query_handler( def prepare(self, reactor: MemoryReactor, clock: Clock, hs: HomeServer) -> None: self.store = hs.get_datastores().main + self.storage_controllers = self.hs.get_storage_controllers() + self.task_scheduler = hs.get_task_scheduler() self.frank = UserID.from_string("@1234abcd:test") self.bob = UserID.from_string("@4567:test") self.alice = UserID.from_string("@alice:remote") self.register_user(self.frank.localpart, "frankpassword") + self.frank_token = self.login(self.frank.localpart, "frankpassword") self.handler = hs.get_profile_handler() @@ -113,6 +125,195 @@ def test_set_my_name(self) -> None: self.get_success(self.store.get_profile_displayname(self.frank)) ) + def test_update_room_membership_on_set_displayname(self) -> None: + """Test that `set_displayname` updates membership events in rooms.""" + + self.get_success( + self.handler.set_displayname( + self.frank, synapse.types.create_requester(self.frank), "Frank" + ) + ) + + room_id = self.helper.create_room_as( + self.frank.to_string(), tok=self.frank_token + ) + + state_tuple = (EventTypes.Member, self.frank.to_string()) + + membership = self.get_success( + self.storage_controllers.state.get_current_state( + room_id, StateFilter.from_types([state_tuple]) + ) + ) + self.assertEqual(membership[state_tuple].content["displayname"], "Frank") + + self.get_success( + self.handler.set_displayname( + self.frank, synapse.types.create_requester(self.frank), "Frank Jr." + ) + ) + + membership = self.get_success( + self.storage_controllers.state.get_current_state( + room_id, StateFilter.from_types([state_tuple]) + ) + ) + self.assertEqual(membership[state_tuple].content["displayname"], "Frank Jr.") + + def test_background_update_room_membership_on_set_displayname(self) -> None: + """Test that `set_displayname` returns immediately and that room membership updates are still done in background.""" + + self.get_success( + self.handler.set_displayname( + self.frank, synapse.types.create_requester(self.frank), "Frank" + ) + ) + + room_id = self.helper.create_room_as( + self.frank.to_string(), tok=self.frank_token + ) + + original_update_membership = self.hs.get_room_member_handler().update_membership + + async def slow_update_membership(*args: Any, **kwargs: Any) -> tuple[str, int]: + await self.clock.sleep(Duration(milliseconds=10)) + return await original_update_membership(*args, **kwargs) + + with patch.object( + self.hs.get_room_member_handler(), + "update_membership", + side_effect=slow_update_membership, + ): + state_tuple = (EventTypes.Member, self.frank.to_string()) + self.get_success( + self.handler.set_displayname( + self.frank, synapse.types.create_requester(self.frank), "Frank Jr." + ) + ) + + membership = self.get_success( + self.storage_controllers.state.get_current_state( + room_id, StateFilter.from_types([state_tuple]) + ) + ) + self.assertEqual(membership[state_tuple].content["displayname"], "Frank") + + # Let's be sure we are over the delay introduced by slow_update_membership + self.get_success(self.clock.sleep(Duration(milliseconds=20)), by=1) + + membership = self.get_success( + self.storage_controllers.state.get_current_state( + room_id, StateFilter.from_types([state_tuple]) + ) + ) + self.assertEqual( + membership[state_tuple].content["displayname"], "Frank Jr." + ) + + def test_background_update_room_membership_resume_after_restart(self) -> None: + """Test that room membership updates triggered by changing the avatar or the display name are resumed after a restart.""" + + self.get_success( + self.handler.set_displayname( + self.frank, synapse.types.create_requester(self.frank), "Frank" + ) + ) + + room_id_1 = self.helper.create_room_as( + self.frank.to_string(), tok=self.frank_token + ) + + room_id_2 = self.helper.create_room_as( + self.frank.to_string(), tok=self.frank_token + ) + + # Ensure `room_id_1` comes before `room_id_2` alphabetically + if room_id_1 > room_id_2: + room_id_1, room_id_2 = room_id_2, room_id_1 + + original_update_membership = self.hs.get_room_member_handler().update_membership + + room_1_updated = False + + async def potentially_slow_update_membership( + *args: Any, **kwargs: Any + ) -> tuple[str, int]: + if args[2] == room_id_2: + await self.clock.sleep(Duration(milliseconds=10)) + if args[2] == room_id_1: + nonlocal room_1_updated + room_1_updated = True + return await original_update_membership(*args, **kwargs) + + with patch.object( + self.hs.get_room_member_handler(), + "update_membership", + side_effect=potentially_slow_update_membership, + ): + state_tuple = (EventTypes.Member, self.frank.to_string()) + self.get_success( + self.handler.set_displayname( + self.frank, synapse.types.create_requester(self.frank), "Frank Jr." + ) + ) + + # Check that the displayname is updated immediately for the first room + membership = self.get_success( + self.storage_controllers.state.get_current_state( + room_id_1, StateFilter.from_types([state_tuple]) + ) + ) + self.assertEqual( + membership[state_tuple].content["displayname"], "Frank Jr." + ) + + # Simulate a synapse restart by emptying the list of running tasks + # and canceling the deferred + _, deferred = self.task_scheduler._running_tasks.popitem() + deferred.cancel() + + # Let's reset the flag to track whether room 1 was updated after the restart + room_1_updated = False + + # Let's be sure we are over the delay introduced by slow_update_membership + # and that the task was not executed as expected + self.get_success(self.clock.sleep(Duration(milliseconds=20)), by=1) + + membership = self.get_success( + self.storage_controllers.state.get_current_state( + room_id_2, StateFilter.from_types([state_tuple]) + ) + ) + self.assertEqual(membership[state_tuple].content["displayname"], "Frank") + + cancelled_task = self.get_success( + self.task_scheduler.get_tasks( + actions=["update_join_states"], statuses=[TaskStatus.CANCELLED] + ) + )[0] + + self.get_success( + self.task_scheduler.update_task( + cancelled_task.id, status=TaskStatus.ACTIVE + ) + ) + + # Let's be sure we are over the delay introduced by slow_update_membership + self.get_success(self.clock.sleep(Duration(milliseconds=20)), by=1) + + # Updates should have been resumed from room 2 after the restart + # so room 1 should not have been updated this time + self.assertFalse(room_1_updated) + + membership = self.get_success( + self.storage_controllers.state.get_current_state( + room_id_2, StateFilter.from_types([state_tuple]) + ) + ) + self.assertEqual( + membership[state_tuple].content["displayname"], "Frank Jr." + ) + def test_set_my_name_if_disabled(self) -> None: self.hs.config.registration.enable_set_displayname = False From 6e21f9c12bfc10f95d41bfef5a456cf5e2dabc2c Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Thu, 5 Mar 2026 11:30:52 -0700 Subject: [PATCH 054/121] Add unstable federation API for MSC4370 `GET /extremities` (#19314) MSC (recommended reading): https://github.com/matrix-org/matrix-spec-proposals/pull/4370 ### Pull Request Checklist * [x] Pull request is based on the develop branch * [x] Pull request includes a [changelog file](https://element-hq.github.io/synapse/latest/development/contributing_guide.html#changelog). The entry should: - Be a short description of your change which makes sense to users. "Fixed a bug that prevented receiving messages from other servers." instead of "Moved X method from `EventStore` to `EventWorkerStore`.". - Use markdown where necessary, mostly for `code blocks`. - End with either a period (.) or an exclamation mark (!). - Start with a capital letter. - Feel free to credit yourself, by adding a sentence "Contributed by @github_username." or "Contributed by [Your Name]." to the end of the entry. * [x] [Code style](https://element-hq.github.io/synapse/latest/code_style.html) is correct (run the [linters](https://element-hq.github.io/synapse/latest/development/contributing_guide.html#run-the-linters)) --------- Co-authored-by: turt2live <1190097+turt2live@users.noreply.github.com> Co-authored-by: Olivier 'reivilibre' --- changelog.d/19314.feature | 1 + synapse/config/experimental.py | 5 + synapse/federation/federation_server.py | 13 ++ .../federation/transport/server/__init__.py | 8 ++ .../federation/transport/server/federation.py | 20 ++- tests/federation/test_federation_server.py | 135 ++++++++++++++++++ 6 files changed, 181 insertions(+), 1 deletion(-) create mode 100644 changelog.d/19314.feature diff --git a/changelog.d/19314.feature b/changelog.d/19314.feature new file mode 100644 index 00000000000..fd2893c577d --- /dev/null +++ b/changelog.d/19314.feature @@ -0,0 +1 @@ +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/synapse/config/experimental.py b/synapse/config/experimental.py index 76e8de35fb8..a8c9305704e 100644 --- a/synapse/config/experimental.py +++ b/synapse/config/experimental.py @@ -3,6 +3,7 @@ # # Copyright 2021 The Matrix.org Foundation C.I.C. # Copyright (C) 2023 New Vector, Ltd +# Copyright (C) 2025 Element Creations Ltd # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License as @@ -517,6 +518,10 @@ def read_config( "msc4108_delegation_endpoint", None ) + # MSC4370: Get extremities federation endpoint + # See https://github.com/element-hq/synapse/issues/19524 + self.msc4370_enabled = experimental.get("msc4370_enabled", False) + auth_delegated = self.msc3861.enabled or ( config.get("matrix_authentication_service") or {} ).get("enabled", False) diff --git a/synapse/federation/federation_server.py b/synapse/federation/federation_server.py index 1912d545f55..eff6d637894 100644 --- a/synapse/federation/federation_server.py +++ b/synapse/federation/federation_server.py @@ -4,6 +4,7 @@ # Copyright 2019-2021 Matrix.org Federation C.I.C # Copyright 2015, 2016 OpenMarket Ltd # Copyright (C) 2023 New Vector, Ltd +# Copyright (C) 2025 Element Creations Ltd # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License as @@ -683,6 +684,18 @@ async def on_query_request( resp = await self.registry.on_query(query_type, args) return 200, resp + async def on_get_extremities_request(self, origin: str, room_id: str) -> JsonDict: + # Assert host in room first to hide contents of the ACL from the caller + await self._event_auth_handler.assert_host_in_room(room_id, origin) + origin_host, _ = parse_server_name(origin) + await self.check_server_matches_acl(origin_host, room_id) + + extremities = await self.store.get_forward_extremities_for_room(room_id) + prev_event_ids = [event_id for event_id, _, _, _ in extremities] + if len(prev_event_ids) == 0: + raise SynapseError(500, "Room has no forward extremities") + return {"prev_events": prev_event_ids} + async def on_make_join_request( self, origin: str, room_id: str, user_id: str, supported_versions: list[str] ) -> dict[str, Any]: diff --git a/synapse/federation/transport/server/__init__.py b/synapse/federation/transport/server/__init__.py index 6d92d005238..0eff49cf73c 100644 --- a/synapse/federation/transport/server/__init__.py +++ b/synapse/federation/transport/server/__init__.py @@ -4,6 +4,7 @@ # Copyright 2020 Sorunome # Copyright 2014-2021 The Matrix.org Foundation C.I.C. # Copyright (C) 2023 New Vector, Ltd +# Copyright (C) 2025 Element Creations Ltd # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License as @@ -33,6 +34,7 @@ FederationMediaDownloadServlet, FederationMediaThumbnailServlet, FederationUnstableClientKeysClaimServlet, + FederationUnstableGetExtremitiesServlet, ) from synapse.http.server import HttpServer, JsonResource from synapse.http.servlet import ( @@ -326,6 +328,12 @@ def register_servlets( if not hs.config.media.can_load_media_repo: continue + if ( + servletclass == FederationUnstableGetExtremitiesServlet + and not hs.config.experimental.msc4370_enabled + ): + continue + servletclass( hs=hs, authenticator=authenticator, diff --git a/synapse/federation/transport/server/federation.py b/synapse/federation/transport/server/federation.py index a7c297c0b76..d783e6da518 100644 --- a/synapse/federation/transport/server/federation.py +++ b/synapse/federation/transport/server/federation.py @@ -1,8 +1,9 @@ # # This file is licensed under the Affero General Public License (AGPL) version 3. # -# Copyright 2021 The Matrix.org Foundation C.I.C. +# Copyright 2021 The Matrix.org Foundation C.I.C. # Copyright (C) 2023 New Vector, Ltd +# Copyright (C) 2025 Element Creations Ltd # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License as @@ -273,6 +274,22 @@ async def on_GET( return await self.handler.on_query_request(query_type, args) +class FederationUnstableGetExtremitiesServlet(BaseFederationServerServlet): + PREFIX = FEDERATION_UNSTABLE_PREFIX + "/org.matrix.msc4370" + PATH = "/extremities/(?P[^/]*)" + CATEGORY = "Federation requests" + + async def on_GET( + self, + origin: str, + content: Literal[None], + query: dict[bytes, list[bytes]], + room_id: str, + ) -> tuple[int, JsonDict]: + result = await self.handler.on_get_extremities_request(origin, room_id) + return 200, result + + class FederationMakeJoinServlet(BaseFederationServerServlet): PATH = "/make_join/(?P[^/]*)/(?P[^/]*)" CATEGORY = "Federation requests" @@ -884,6 +901,7 @@ async def on_GET( FederationBackfillServlet, FederationTimestampLookupServlet, FederationQueryServlet, + FederationUnstableGetExtremitiesServlet, FederationMakeJoinServlet, FederationMakeLeaveServlet, FederationEventServlet, diff --git a/tests/federation/test_federation_server.py b/tests/federation/test_federation_server.py index c4491d5b3c9..275e5dfa1d8 100644 --- a/tests/federation/test_federation_server.py +++ b/tests/federation/test_federation_server.py @@ -324,6 +324,141 @@ def test_needs_to_be_in_room(self) -> None: self.assertEqual(channel.json_body["errcode"], "M_FORBIDDEN") +class UnstableGetExtremitiesTests(unittest.FederatingHomeserverTestCase): + servlets = [ + admin.register_servlets, + room.register_servlets, + login.register_servlets, + ] + + def prepare(self, reactor: MemoryReactor, clock: Clock, hs: HomeServer) -> None: + super().prepare(reactor, clock, hs) + self._storage_controllers = hs.get_storage_controllers() + + def _make_endpoint_path(self, room_id: str) -> str: + return f"/_matrix/federation/unstable/org.matrix.msc4370/extremities/{room_id}" + + def _remote_join(self, room_id: str, room_version: str) -> str: + # Note: other tests ensure the called endpoints in this function return useful + # and proper data. + + # make_join first + joining_user = "@misspiggy:" + self.OTHER_SERVER_NAME + channel = self.make_signed_federation_request( + "GET", + f"/_matrix/federation/v1/make_join/{room_id}/{joining_user}?ver={room_version}", + ) + self.assertEqual(channel.code, HTTPStatus.OK, channel.json_body) + join_result = channel.json_body + + # Sign/populate the join + join_event_dict = join_result["event"] + self.add_hashes_and_signatures_from_other_server( + join_event_dict, + KNOWN_ROOM_VERSIONS[room_version], + ) + if room_version in ["1", "2"]: + add_hashes_and_signatures( + KNOWN_ROOM_VERSIONS[room_version], + join_event_dict, + signature_name=self.hs.hostname, + signing_key=self.hs.signing_key, + ) + + # Send the join + channel = self.make_signed_federation_request( + "PUT", + f"/_matrix/federation/v2/send_join/{room_id}/x", + content=join_event_dict, + ) + + # Check that things went okay so the test doesn't become a total train wreck + self.assertEqual(channel.code, HTTPStatus.OK, channel.json_body) + r = self.get_success(self._storage_controllers.state.get_current_state(room_id)) + self.assertEqual(r[("m.room.member", joining_user)].membership, "join") + + return r[("m.room.member", joining_user)].event_id + + def _test_get_extremities_common(self, room_version: str) -> None: + # Create a room to test with + creator_user_id = self.register_user("kermit", "test") + tok = self.login("kermit", "test") + room_id = self.helper.create_room_as( + room_creator=creator_user_id, + tok=tok, + room_version=room_version, + extra_content={ + # Public preset uses `shared` history visibility, but makes joins + # easier in our tests. + # https://spec.matrix.org/v1.16/client-server-api/#post_matrixclientv3createroom + "preset": "public_chat" + }, + ) + + # At this stage we should fail to get the extremities because we're not joined + # and therefore can't see the events (`shared` history visibility). + channel = self.make_signed_federation_request( + "GET", self._make_endpoint_path(room_id) + ) + self.assertEqual(channel.code, HTTPStatus.FORBIDDEN, channel.json_body) + self.assertEqual(channel.json_body["error"], "Host not in room.") + self.assertEqual(channel.json_body["errcode"], "M_FORBIDDEN") + + # Now join the room and try again + # Note: there should be just one extremity: the join + join_event_id = self._remote_join(room_id, room_version) + channel = self.make_signed_federation_request( + "GET", self._make_endpoint_path(room_id) + ) + self.assertEqual(channel.code, HTTPStatus.OK, channel.json_body) + self.assertEqual(channel.json_body["prev_events"], [join_event_id]) + + # ACL the calling server and try again. This should cause an error getting extremities. + self.helper.send_state( + room_id, + "m.room.server_acl", + { + "allow": ["*"], + "allow_ip_literals": False, + "deny": [self.OTHER_SERVER_NAME], + }, + tok=tok, + expect_code=HTTPStatus.OK, + ) + channel = self.make_signed_federation_request( + "GET", self._make_endpoint_path(room_id) + ) + self.assertEqual(channel.code, HTTPStatus.FORBIDDEN, channel.json_body) + self.assertEqual(channel.json_body["error"], "Server is banned from room") + self.assertEqual(channel.json_body["errcode"], "M_FORBIDDEN") + + @parameterized.expand([(k,) for k in KNOWN_ROOM_VERSIONS.keys()]) + @override_config( + {"use_frozen_dicts": True, "experimental_features": {"msc4370_enabled": True}} + ) + def test_get_extremities_with_frozen_dicts(self, room_version: str) -> None: + """Test GET /extremities with USE_FROZEN_DICTS=True""" + self._test_get_extremities_common(room_version) + + @parameterized.expand([(k,) for k in KNOWN_ROOM_VERSIONS.keys()]) + @override_config( + {"use_frozen_dicts": False, "experimental_features": {"msc4370_enabled": True}} + ) + def test_get_extremities_without_frozen_dicts(self, room_version: str) -> None: + """Test GET /extremities with USE_FROZEN_DICTS=False""" + self._test_get_extremities_common(room_version) + + # note the lack of config-setting stuff on this test. + def test_get_extremities_unstable_not_enabled(self) -> None: + """Test that GET /extremities returns M_UNRECOGNIZED when MSC4370 is not enabled""" + # We shouldn't even have to create a room - the endpoint should just fail. + channel = self.make_signed_federation_request( + "GET", self._make_endpoint_path("!room:example.org") + ) + self.assertEqual(channel.code, HTTPStatus.NOT_FOUND, channel.json_body) + self.assertEqual(channel.json_body["errcode"], "M_UNRECOGNIZED") + + class SendJoinFederationTests(unittest.FederatingHomeserverTestCase): servlets = [ admin.register_servlets, From 16125cecd2d60a77aa31a02d6cc293cdd7dae603 Mon Sep 17 00:00:00 2001 From: Quentin Gliech Date: Mon, 9 Mar 2026 16:11:04 +0100 Subject: [PATCH 055/121] Remove the optional `systemd-python` dependency (#19491) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Summary - drop the `systemd` extra from `pyproject.toml` and the `systemd-python` optional dependency - this means we don't ship the journald log handler, so it clarifies the docs how to install that in the venv - ensure the Debian virtualenv build keeps shipping `systemd-python>=231` in the venv, so the packaged log config can keep using `systemd.journal.JournalHandler` Context of this is the following: > Today in my 'how hard would it be to move to uv' journey: https://github.com/systemd/python-systemd/issues/167 > > The gist of it is that uv really wants to create a universal lock file, which means it needs to be able to resolve the package metadata, even for packages locked for other platforms. In the case of systemd-python, they use mesonpy as build backend, which doesn't implement prepare_metadata_for_build_wheel, which means it needs to run meson to be able to resolve the package metadata. And it will hard-fail if libsystemd dev headers aren't available 😭 > > [*message in #synapse-dev:matrix.org*](https://matrix.to/#/!i5D5LLct_DYG-4hQprLzrxdbZ580U9UB6AEgFnk6rZQ/$OKLB3TJVXAwq43sAZFJ-_PvMMzl4P_lWmSAtlmsoMuM?via=element.io&via=matrix.org&via=beeper.com) --- changelog.d/19491.misc | 1 + contrib/systemd/README.md | 6 ++++++ contrib/systemd/log_config.yaml | 1 + debian/build_virtualenv | 6 ++++-- debian/changelog | 6 ++++++ docs/setup/installation.md | 5 +++++ docs/upgrade.md | 16 ++++++++++++++++ .../usage/configuration/logging_sample_config.md | 3 +++ poetry.lock | 15 +-------------- pyproject.toml | 5 ----- 10 files changed, 43 insertions(+), 21 deletions(-) create mode 100644 changelog.d/19491.misc diff --git a/changelog.d/19491.misc b/changelog.d/19491.misc new file mode 100644 index 00000000000..62b0ddddc2c --- /dev/null +++ b/changelog.d/19491.misc @@ -0,0 +1 @@ +Remove the optional `systemd-python` dependency and the `systemd` extra on the `synapse` package. diff --git a/contrib/systemd/README.md b/contrib/systemd/README.md index d12d8ec37bd..456b4e9a160 100644 --- a/contrib/systemd/README.md +++ b/contrib/systemd/README.md @@ -16,3 +16,9 @@ appropriate locations of your installation. 5. Start Synapse: `sudo systemctl start matrix-synapse` 6. Verify Synapse is running: `sudo systemctl status matrix-synapse` 7. *optional* Enable Synapse to start at system boot: `sudo systemctl enable matrix-synapse` + +## Logging + +If you use `contrib/systemd/log_config.yaml`, install `systemd-python` in the +same Python environment as Synapse. The config uses +`systemd.journal.JournalHandler`, which requires that package. diff --git a/contrib/systemd/log_config.yaml b/contrib/systemd/log_config.yaml index 22f67a50ce0..4c297876477 100644 --- a/contrib/systemd/log_config.yaml +++ b/contrib/systemd/log_config.yaml @@ -1,5 +1,6 @@ version: 1 +# Requires the `systemd-python` package in Synapse's runtime environment. # In systemd's journal, loglevel is implicitly stored, so let's omit it # from the message text. formatters: diff --git a/debian/build_virtualenv b/debian/build_virtualenv index 9e7fb95c8e0..70d4efcbd0d 100755 --- a/debian/build_virtualenv +++ b/debian/build_virtualenv @@ -39,8 +39,10 @@ pip install poetry==2.1.1 poetry-plugin-export==1.9.0 poetry export \ --extras all \ --extras test \ - --extras systemd \ -o exported_requirements.txt +# Keep journald logging support in the packaged virtualenv when using +# systemd.journal.JournalHandler in /etc/matrix-synapse/log.yaml. +echo "systemd-python==235 --hash=sha256:4e57f39797fd5d9e2d22b8806a252d7c0106c936039d1e71c8c6b8008e695c0a" >> exported_requirements.txt deactivate rm -rf "$TEMP_VENV" @@ -57,7 +59,7 @@ dh_virtualenv \ --extra-pip-arg="--no-deps" \ --extra-pip-arg="--no-cache-dir" \ --extra-pip-arg="--compile" \ - --extras="all,systemd,test" \ + --extras="all,test" \ --requirements="exported_requirements.txt" PACKAGE_BUILD_DIR="$(pwd)/debian/matrix-synapse-py3" diff --git a/debian/changelog b/debian/changelog index 583bb35ad5b..5867465aede 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,3 +1,9 @@ +matrix-synapse-py3 (1.149.0~rc1+nmu1) UNRELEASED; urgency=medium + + * Change how the systemd journald integration is installed. + + -- Quentin Gliech Fri, 20 Feb 2026 19:19:51 +0100 + matrix-synapse-py3 (1.149.0~rc1) stable; urgency=medium * New synapse release 1.149.0rc1. diff --git a/docs/setup/installation.md b/docs/setup/installation.md index a48662362af..c887f9900cc 100644 --- a/docs/setup/installation.md +++ b/docs/setup/installation.md @@ -229,6 +229,11 @@ pip install --upgrade setuptools pip install matrix-synapse ``` +If you want to use a logging configuration that references +`systemd.journal.JournalHandler` (for example `contrib/systemd/log_config.yaml`), +you must install `systemd-python` separately in the same environment. +Synapse no longer provides a `matrix-synapse[systemd]` extra. + This will download Synapse from [PyPI](https://pypi.org/project/matrix-synapse) and install it, along with the python libraries it uses, into a virtual environment under `~/synapse/env`. Feel free to pick a different directory if you diff --git a/docs/upgrade.md b/docs/upgrade.md index 1630c6ab407..0e2005f2820 100644 --- a/docs/upgrade.md +++ b/docs/upgrade.md @@ -117,6 +117,22 @@ each upgrade are complete before moving on to the next upgrade, to avoid stacking them up. You can monitor the currently running background updates with [the Admin API](usage/administration/admin_api/background_updates.html#status). +# Upgrading to v1.150.0 + +## Removal of the `systemd` pip extra + +The `matrix-synapse[systemd]` pip extra has been removed. +If you use `systemd.journal.JournalHandler` in your logging configuration +(e.g. `contrib/systemd/log_config.yaml`), you must now install +`systemd-python` manually in Synapse's runtime environment: + +```bash +pip install systemd-python +``` + +No action is needed if you do not use journal logging, or if you installed +Synapse from the Debian packages (which handle this automatically). + # 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/configuration/logging_sample_config.md b/docs/usage/configuration/logging_sample_config.md index 23a55abdccd..9cc96df73c2 100644 --- a/docs/usage/configuration/logging_sample_config.md +++ b/docs/usage/configuration/logging_sample_config.md @@ -14,6 +14,9 @@ It should be named `.log.config` by default. Hint: If you're looking for a guide on what each of the fields in the "Processed request" log lines mean, see [Request log format](../administration/request_log.md). +If you use `systemd.journal.JournalHandler` in your own logging config, ensure +`systemd-python` is installed in Synapse's runtime environment. + ```yaml {{#include ../../sample_log_config.yaml}} ``` diff --git a/poetry.lock b/poetry.lock index 7f9b34d38f8..0df6addb3a2 100644 --- a/poetry.lock +++ b/poetry.lock @@ -3128,18 +3128,6 @@ c = ["sqlglotc"] dev = ["duckdb (>=0.6)", "mypy", "pandas", "pandas-stubs", "pdoc", "pre-commit", "pyperf", "python-dateutil", "pytz", "ruff (==0.7.2)", "types-python-dateutil", "types-pytz", "typing_extensions"] rs = ["sqlglotrs (==0.13.0)"] -[[package]] -name = "systemd-python" -version = "235" -description = "Python interface for libsystemd" -optional = true -python-versions = "*" -groups = ["main"] -markers = "extra == \"systemd\"" -files = [ - {file = "systemd-python-235.tar.gz", hash = "sha256:4e57f39797fd5d9e2d22b8806a252d7c0106c936039d1e71c8c6b8008e695c0a"}, -] - [[package]] name = "threadloop" version = "1.0.2" @@ -3761,11 +3749,10 @@ postgres = ["psycopg2", "psycopg2cffi", "psycopg2cffi-compat"] redis = ["hiredis", "txredisapi"] saml2 = ["defusedxml", "pysaml2", "pytz"] sentry = ["sentry-sdk"] -systemd = ["systemd-python"] test = ["idna", "parameterized"] url-preview = ["lxml"] [metadata] lock-version = "2.1" python-versions = ">=3.10.0,<4.0.0" -content-hash = "1caa5072f6304122c89377420f993a54f54587f3618ccc8094ec31642264592c" +content-hash = "dd63614889e7e181fca33760741a490e65fe4ef4f42756cafd0f804ae7324916" diff --git a/pyproject.toml b/pyproject.toml index 9d5e3fec2d6..b8564816ce2 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -135,10 +135,6 @@ saml2 = [ "pytz>=2018.3", # via pysaml2 ] oidc = ["authlib>=0.15.1"] -# systemd-python is necessary for logging to the systemd journal via -# `systemd.journal.JournalHandler`, as is documented in -# `contrib/systemd/log_config.yaml`. -systemd = ["systemd-python>=231"] url-preview = ["lxml>=4.6.3"] sentry = ["sentry-sdk>=0.7.2"] opentracing = [ @@ -194,7 +190,6 @@ all = [ "pympler>=1.0", # omitted: # - test: it's useful to have this separate from dev deps in the olddeps job - # - systemd: this is a system-based requirement # Transitive dependencies # These dependencies aren't directly required by Synapse. From 6e1ac551f4485c24a909109e25bb5cbfa0ba9640 Mon Sep 17 00:00:00 2001 From: Olivier 'reivilibre Date: Tue, 10 Mar 2026 10:39:39 +0000 Subject: [PATCH 056/121] Expose MSC4354 Sticky Events over the legacy (v3) /sync API. (#19487) Follows: #19365 Part of: MSC4354 whose experimental feature tracking issue is #19409 Partially supersedes: #18968 --------- Signed-off-by: Olivier 'reivilibre' --- changelog.d/19487.feature | 1 + synapse/handlers/sync.py | 125 ++++- synapse/rest/client/sync.py | 8 + .../storage/databases/main/sticky_events.py | 97 +++- tests/rest/client/test_sync_sticky_events.py | 529 ++++++++++++++++++ 5 files changed, 749 insertions(+), 11 deletions(-) create mode 100644 changelog.d/19487.feature create mode 100644 tests/rest/client/test_sync_sticky_events.py diff --git a/changelog.d/19487.feature b/changelog.d/19487.feature new file mode 100644 index 00000000000..4eb1d8f2617 --- /dev/null +++ b/changelog.d/19487.feature @@ -0,0 +1 @@ +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/synapse/handlers/sync.py b/synapse/handlers/sync.py index a32748c2a9c..c8ef5e2aa6c 100644 --- a/synapse/handlers/sync.py +++ b/synapse/handlers/sync.py @@ -37,6 +37,7 @@ EventContentFields, EventTypes, Membership, + StickyEvent, ) from synapse.api.filtering import FilterCollection from synapse.api.presence import UserPresenceState @@ -147,6 +148,7 @@ class JoinedSyncResult: state: StateMap[EventBase] ephemeral: list[JsonDict] account_data: list[JsonDict] + sticky: list[EventBase] unread_notifications: JsonDict unread_thread_notifications: JsonDict summary: JsonDict | None @@ -157,7 +159,11 @@ def __bool__(self) -> bool: to tell if room needs to be part of the sync result. """ return bool( - self.timeline or self.state or self.ephemeral or self.account_data + self.timeline + or self.state + or self.ephemeral + or self.account_data + or self.sticky # nb the notification count does not, er, count: if there's nothing # else in the result, we don't need to send it. ) @@ -601,6 +607,41 @@ async def ephemeral_by_room( return now_token, ephemeral_by_room + async def sticky_events_by_room( + self, + sync_result_builder: "SyncResultBuilder", + now_token: StreamToken, + since_token: StreamToken | None = None, + ) -> tuple[StreamToken, dict[str, list[str]]]: + """Get the sticky events for each room the user is in + Args: + sync_result_builder + now_token: Where the server is currently up to. + since_token: Where the server was when the client last synced. + Returns: + A tuple of the now StreamToken, updated to reflect the which sticky + events are included, and a dict mapping from room_id to a list + of sticky event IDs for that room (in sticky event stream order). + """ + now = self.clock.time_msec() + with Measure( + self.clock, name="sticky_events_by_room", server_name=self.server_name + ): + from_id = since_token.sticky_events_key if since_token else 0 + + room_ids = sync_result_builder.joined_room_ids + + to_id, sticky_by_room = await self.store.get_sticky_events_in_rooms( + room_ids, + from_id=from_id, + to_id=now_token.sticky_events_key, + now=now, + limit=StickyEvent.MAX_EVENTS_IN_SYNC, + ) + now_token = now_token.copy_and_replace(StreamKeyType.STICKY_EVENTS, to_id) + + return now_token, sticky_by_room + async def _load_filtered_recents( self, room_id: str, @@ -2177,11 +2218,43 @@ async def _generate_sync_entry_for_rooms( ) sync_result_builder.now_token = now_token + sticky_by_room: dict[str, list[str]] = {} + if self.hs_config.experimental.msc4354_enabled: + now_token, sticky_by_room = await self.sticky_events_by_room( + sync_result_builder, now_token, since_token + ) + sync_result_builder.now_token = now_token + # 2. We check up front if anything has changed, if it hasn't then there is # no point in going further. + # + # If this is an initial sync (no since_token), then of course we can't skip + # the sync entry, as we have no base to use as a comparison for the question + # 'has anything changed' (this is the client's first time 'seeing' anything). + # + # Otherwise, for incremental syncs, we consider skipping the sync entry, + # doing cheap checks first: + # + # - are there any per-room EDUs; + # - is there any Room Account Data; or + # - are there any sticky events in the rooms; or + # - might the rooms have changed + # (using in-memory event stream change caches, which can + # only answer either 'Not changed' or 'Possibly changed') + # + # If none of those cheap checks give us a reason to continue generating the sync entry, + # we finally query the database to check for changed room tags. + # If there are also no changed tags, we can short-circuit return an empty sync entry. if not sync_result_builder.full_state: - if since_token and not ephemeral_by_room and not account_data_by_room: - have_changed = await self._have_rooms_changed(sync_result_builder) + # Cheap checks first + if ( + since_token + and not ephemeral_by_room + and not account_data_by_room + and not sticky_by_room + ): + # This is also a cheap check, but we log the answer + have_changed = self._may_have_rooms_changed(sync_result_builder) log_kv({"rooms_have_changed": have_changed}) if not have_changed: tags_by_room = await self.store.get_updated_tags( @@ -2225,6 +2298,7 @@ async def handle_room_entries(room_entry: "RoomSyncResultBuilder") -> None: ephemeral=ephemeral_by_room.get(room_entry.room_id, []), tags=tags_by_room.get(room_entry.room_id), account_data=account_data_by_room.get(room_entry.room_id, {}), + sticky_event_ids=sticky_by_room.get(room_entry.room_id, []), always_include=sync_result_builder.full_state, ) logger.debug("Generated room entry for %s", room_entry.room_id) @@ -2237,11 +2311,9 @@ async def handle_room_entries(room_entry: "RoomSyncResultBuilder") -> None: return set(newly_joined_rooms), set(newly_left_rooms) - async def _have_rooms_changed( - self, sync_result_builder: "SyncResultBuilder" - ) -> bool: + def _may_have_rooms_changed(self, sync_result_builder: "SyncResultBuilder") -> bool: """Returns whether there may be any new events that should be sent down - the sync. Returns True if there are. + the sync. Returns True if there **may** be. Does not modify the `sync_result_builder`. """ @@ -2611,6 +2683,7 @@ async def _generate_room_entry( ephemeral: list[JsonDict], tags: Mapping[str, JsonMapping] | None, account_data: Mapping[str, JsonMapping], + sticky_event_ids: list[str], always_include: bool = False, ) -> None: """Populates the `joined` and `archived` section of `sync_result_builder` @@ -2640,6 +2713,8 @@ async def _generate_room_entry( tags: List of *all* tags for room, or None if there has been no change. account_data: List of new account data for room + sticky_event_ids: MSC4354 sticky events in the room, if any. + In sticky event stream order. always_include: Always include this room in the sync response, even if empty. """ @@ -2650,7 +2725,13 @@ async def _generate_room_entry( events = room_builder.events # We want to shortcut out as early as possible. - if not (always_include or account_data or ephemeral or full_state): + if not ( + always_include + or account_data + or ephemeral + or full_state + or sticky_event_ids + ): if events == [] and tags is None: return @@ -2742,6 +2823,7 @@ async def _generate_room_entry( or account_data_events or ephemeral or full_state + or sticky_event_ids ): return @@ -2788,6 +2870,32 @@ async def _generate_room_entry( if room_builder.rtype == "joined": unread_notifications: dict[str, int] = {} + sticky_events: list[EventBase] = [] + if sticky_event_ids: + # As per MSC4354: + # Remove sticky events that are already in the timeline, else we will needlessly duplicate + # events. + # There is no purpose in including sticky events in the sticky section if they're already in + # the timeline, as either way the client becomes aware of them. + # This is particularly important given the risk of sticky events spam since + # anyone can send sticky events, so halving the bandwidth on average for each sticky + # event is helpful. + timeline_event_id_set = {ev.event_id for ev in batch.events} + # Must preserve sticky event stream order + sticky_event_ids = [ + e for e in sticky_event_ids if e not in timeline_event_id_set + ] + if sticky_event_ids: + # Fetch and filter the sticky events + sticky_events = await filter_and_transform_events_for_client( + self._storage_controllers, + sync_result_builder.sync_config.user.to_string(), + await self.store.get_events_as_list(sticky_event_ids), + # As per MSC4354: + # > History visibility checks MUST NOT be applied to sticky events. + # > Any joined user is authorised to see sticky events for the duration they remain sticky. + always_include_ids=frozenset(sticky_event_ids), + ) room_sync = JoinedSyncResult( room_id=room_id, timeline=batch, @@ -2798,6 +2906,7 @@ async def _generate_room_entry( unread_thread_notifications={}, summary=summary, unread_count=0, + sticky=sticky_events, ) if room_sync or always_include: diff --git a/synapse/rest/client/sync.py b/synapse/rest/client/sync.py index 91f2f16771a..710d097eab0 100644 --- a/synapse/rest/client/sync.py +++ b/synapse/rest/client/sync.py @@ -619,6 +619,14 @@ async def encode_room( ephemeral_events = room.ephemeral result["ephemeral"] = {"events": ephemeral_events} result["unread_notifications"] = room.unread_notifications + if room.sticky: + # The sticky events have already been deduplicated so that events + # appearing in the timeline won't appear again here + result["msc4354_sticky"] = { + "events": await self._event_serializer.serialize_events( + room.sticky, time_now, config=serialize_options + ) + } if room.unread_thread_notifications: result["unread_thread_notifications"] = room.unread_thread_notifications if self._msc3773_enabled: diff --git a/synapse/storage/databases/main/sticky_events.py b/synapse/storage/databases/main/sticky_events.py index 101306296e4..38b84443df6 100644 --- a/synapse/storage/databases/main/sticky_events.py +++ b/synapse/storage/databases/main/sticky_events.py @@ -13,9 +13,7 @@ import logging import random from dataclasses import dataclass -from typing import ( - TYPE_CHECKING, -) +from typing import TYPE_CHECKING, Collection, cast from twisted.internet.defer import Deferred @@ -25,6 +23,7 @@ DatabasePool, LoggingDatabaseConnection, LoggingTransaction, + make_in_list_sql_clause, ) from synapse.storage.databases.main.cache import CacheInvalidationWorkerStore from synapse.storage.databases.main.state import StateGroupWorkerStore @@ -138,6 +137,98 @@ def get_max_sticky_events_stream_id(self) -> int: def get_sticky_events_stream_id_generator(self) -> MultiWriterIdGenerator: return self._sticky_events_id_gen + async def get_sticky_events_in_rooms( + self, + room_ids: Collection[str], + *, + from_id: int, + to_id: int, + now: int, + limit: int | None, + ) -> tuple[int, dict[str, list[str]]]: + """ + Fetch all the sticky events' IDs in the given rooms, with sticky stream IDs satisfying + from_id < sticky stream ID <= to_id. + + The events are returned ordered by the sticky events stream. + + Args: + room_ids: The room IDs to return sticky events in. + from_id: The sticky stream ID that sticky events should be returned from (exclusive). + to_id: The sticky stream ID that sticky events should end at (inclusive). + now: The current time in unix millis, used for skipping expired events. + limit: Max sticky events to return, or None to apply no limit. + Returns: + to_id, dict[room_id, list[event_ids]] + """ + sticky_events_rows = await self.db_pool.runInteraction( + "get_sticky_events_in_rooms", + self._get_sticky_events_in_rooms_txn, + room_ids, + from_id=from_id, + to_id=to_id, + now=now, + limit=limit, + ) + + if not sticky_events_rows: + return to_id, {} + + # Get stream_id of the last row, which is the highest + new_to_id, _, _ = sticky_events_rows[-1] + + # room ID -> event IDs + room_id_to_event_ids: dict[str, list[str]] = {} + for _, room_id, event_id in sticky_events_rows: + events = room_id_to_event_ids.setdefault(room_id, []) + events.append(event_id) + + return (new_to_id, room_id_to_event_ids) + + def _get_sticky_events_in_rooms_txn( + self, + txn: LoggingTransaction, + room_ids: Collection[str], + *, + from_id: int, + to_id: int, + now: int, + limit: int | None, + ) -> list[tuple[int, str, str]]: + if len(room_ids) == 0: + return [] + room_id_in_list_clause, room_id_in_list_values = make_in_list_sql_clause( + txn.database_engine, "se.room_id", room_ids + ) + limit_clause = "" + limit_params: tuple[int, ...] = () + if limit is not None: + limit_clause = "LIMIT ?" + limit_params = (limit,) + + if isinstance(self.database_engine, PostgresEngine): + expr_soft_failed = "COALESCE(((ej.internal_metadata::jsonb)->>'soft_failed')::boolean, FALSE)" + else: + expr_soft_failed = "COALESCE(ej.internal_metadata->>'soft_failed', FALSE)" + + txn.execute( + f""" + SELECT se.stream_id, se.room_id, event_id + FROM sticky_events se + INNER JOIN event_json ej USING (event_id) + WHERE + NOT {expr_soft_failed} + AND ? < expires_at + AND ? < stream_id + AND stream_id <= ? + AND {room_id_in_list_clause} + ORDER BY stream_id ASC + {limit_clause} + """, + (now, from_id, to_id, *room_id_in_list_values, *limit_params), + ) + return cast(list[tuple[int, str, str]], txn.fetchall()) + async def get_updated_sticky_events( self, *, from_id: int, to_id: int, limit: int ) -> list[StickyEventUpdate]: diff --git a/tests/rest/client/test_sync_sticky_events.py b/tests/rest/client/test_sync_sticky_events.py new file mode 100644 index 00000000000..7a38debdb95 --- /dev/null +++ b/tests/rest/client/test_sync_sticky_events.py @@ -0,0 +1,529 @@ +# +# This file is licensed under the Affero General Public License (AGPL) version 3. +# +# Copyright (C) 2026, Element Creations Ltd. +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# See the GNU Affero General Public License for more details: +# . +import json +import sqlite3 +from dataclasses import dataclass +from unittest.mock import patch +from urllib.parse import quote + +from twisted.internet.testing import MemoryReactor + +from synapse.api.constants import EventTypes, EventUnsignedContentFields, StickyEvent +from synapse.rest import admin +from synapse.rest.client import account_data, login, register, room, sync +from synapse.server import HomeServer +from synapse.types import JsonDict +from synapse.util.clock import Clock +from synapse.util.duration import Duration + +from tests import unittest +from tests.utils import USE_POSTGRES_FOR_TESTS + + +class SyncStickyEventsTestCase(unittest.HomeserverTestCase): + """ + Tests for oldschool (v3) /sync with sticky events (MSC4354) + """ + + if not USE_POSTGRES_FOR_TESTS and sqlite3.sqlite_version_info < (3, 40, 0): + # We need the JSON functionality in SQLite + skip = f"SQLite version is too old to support sticky events: {sqlite3.sqlite_version_info} (See https://github.com/element-hq/synapse/issues/19428)" + + servlets = [ + room.register_servlets, + login.register_servlets, + register.register_servlets, + admin.register_servlets, + sync.register_servlets, + account_data.register_servlets, + ] + + def default_config(self) -> JsonDict: + config = super().default_config() + config["experimental_features"] = {"msc4354_enabled": True} + return config + + def prepare(self, reactor: MemoryReactor, clock: Clock, hs: HomeServer) -> None: + # Register an account + self.user_id = self.register_user("user1", "pass") + self.token = self.login(self.user_id, "pass") + + # Create a room + self.room_id = self.helper.create_room_as(self.user_id, tok=self.token) + + def test_single_sticky_event_appears_in_initial_sync(self) -> None: + """ + Test sending a single sticky event and then doing an initial /sync. + """ + + # Send a sticky event + sticky_event_response = self.helper.send_sticky_event( + self.room_id, + EventTypes.Message, + duration=Duration(minutes=1), + content={"body": "sticky message", "msgtype": "m.text"}, + tok=self.token, + ) + sticky_event_id = sticky_event_response["event_id"] + + # Perform initial sync + channel = self.make_request( + "GET", + "/sync", + access_token=self.token, + ) + + self.assertEqual(channel.code, 200, channel.result) + + # Get timeline events from the sync response + timeline_events = channel.json_body["rooms"]["join"][self.room_id]["timeline"][ + "events" + ] + + # Verify the sticky event is present and has the sticky TTL field + self.assertEqual( + timeline_events[-1]["event_id"], + sticky_event_id, + f"Sticky event {sticky_event_id} not found in sync timeline", + ) + self.assertEqual( + timeline_events[-1]["unsigned"][EventUnsignedContentFields.STICKY_TTL], + # The other 100 ms is advanced in FakeChannel.await_result. + 59_900, + ) + + self.assertNotIn( + "sticky", + channel.json_body["rooms"]["join"][self.room_id], + "Unexpected sticky section of sync response (sticky event should be deduplicated)", + ) + + def test_sticky_event_beyond_timeline_in_initial_sync(self) -> None: + """ + Test that when sending a sticky event which is subsequently + pushed out of the timeline window by other messages, + the sticky event comes down the dedicated sticky section of the sync response. + """ + # Send the first sticky event + first_sticky_response = self.helper.send_sticky_event( + self.room_id, + EventTypes.Message, + duration=Duration(minutes=1), + content={"body": "first sticky", "msgtype": "m.text"}, + tok=self.token, + ) + first_sticky_event_id = first_sticky_response["event_id"] + + # Send 10 regular timeline events, + # in order to push the sticky event out of the timeline window + # that the /sync will get. + regular_event_ids = [] + for i in range(10): + # (Note: each one advances time by 100ms) + response = self.helper.send( + room_id=self.room_id, + body=f"regular message {i}", + tok=self.token, + ) + regular_event_ids.append(response["event_id"]) + + # Send another sticky event + # (Note: this advances time by 100ms) + second_sticky_response = self.helper.send_sticky_event( + self.room_id, + EventTypes.Message, + duration=Duration(minutes=1), + content={"body": "second sticky", "msgtype": "m.text"}, + tok=self.token, + ) + second_sticky_event_id = second_sticky_response["event_id"] + + # Perform initial sync + channel = self.make_request( + "GET", + "/sync", + access_token=self.token, + ) + self.assertEqual(channel.code, 200, channel.result) + + # Get timeline events from the sync response + timeline_events = channel.json_body["rooms"]["join"][self.room_id]["timeline"][ + "events" + ] + timeline_event_ids = [event["event_id"] for event in timeline_events] + sticky_events = channel.json_body["rooms"]["join"][self.room_id][ + "msc4354_sticky" + ]["events"] + + # This is canary to check the test setup is valid and that we're actually excluding the sticky event + # because it's outside the timeline window, not for some other potential reason. + self.assertNotIn( + regular_event_ids[0], + timeline_event_ids, + f"First regular event {regular_event_ids[0]} unexpectedly found in sync timeline (this means our test is invalid)", + ) + + # Assertions for the first sticky event: should be only in sticky section + self.assertNotIn( + first_sticky_event_id, + timeline_event_ids, + f"First sticky event {first_sticky_event_id} unexpectedly found in sync timeline (expected it to be outside the timeline window)", + ) + self.assertEqual( + len(sticky_events), + 1, + f"Expected exactly 1 item in sticky events section, got {sticky_events}", + ) + self.assertEqual(sticky_events[0]["event_id"], first_sticky_event_id) + self.assertEqual( + # The 'missing' 1100 ms were elapsed when sending events + sticky_events[0]["unsigned"][EventUnsignedContentFields.STICKY_TTL], + 58_800, + ) + + # Assertions for the second sticky event: should be only in timeline section + self.assertEqual( + timeline_events[-1]["event_id"], + second_sticky_event_id, + f"Second sticky event {second_sticky_event_id} not found in sync timeline", + ) + self.assertEqual( + timeline_events[-1]["unsigned"][EventUnsignedContentFields.STICKY_TTL], + # The other 100 ms is advanced in FakeChannel.await_result. + 59_900, + ) + # (sticky section: we already checked it only has 1 item and + # that item was the first above) + + def test_sticky_event_filtered_from_timeline_appears_in_sticky_section( + self, + ) -> None: + """ + Test that a sticky event which is excluded from the timeline by a timeline filter + still appears in the sticky section of the sync response. + + > Interaction with RoomFilter: The RoomFilter does not apply to the sticky.events section, + > as it is neither timeline nor state. However, the timeline filter MUST be applied before + > applying the deduplication logic above. In other words, if a sticky event would normally + > appear in both the timeline.events section and the sticky.events section, but is filtered + > out by the timeline filter, the sticky event MUST appear in sticky.events. + > — https://github.com/matrix-org/matrix-spec-proposals/blob/4340903c15e9eab1bfb2f6a31cfa08fd535f7e7c/proposals/4354-sticky-events.md#sync-api-changes + """ + # A filter that excludes message events + filter_json = json.dumps( + { + "room": { + # We only want these io.element.example events + "timeline": {"types": ["io.element.example"]}, + } + } + ) + + # Send a sticky message event (will be filtered by our filter) + sticky_event_id = self.helper.send_sticky_event( + self.room_id, + EventTypes.Message, + duration=Duration(minutes=1), + content={"body": "sticky message", "msgtype": "m.text"}, + tok=self.token, + )["event_id"] + + # Send a non-message event (will pass the filter) + nonmessage_event_id = self.helper.send_event( + room_id=self.room_id, + type="io.element.example", + content={"membership": "join"}, + tok=self.token, + )["event_id"] + + # Perform initial sync with our filter + channel = self.make_request( + "GET", + f"/sync?filter={quote(filter_json)}", + access_token=self.token, + ) + self.assertEqual(channel.code, 200, channel.result) + + # Get timeline and sticky events from the sync response + timeline_events = channel.json_body["rooms"]["join"][self.room_id]["timeline"][ + "events" + ] + sticky_events = channel.json_body["rooms"]["join"][self.room_id][ + "msc4354_sticky" + ]["events"] + + # Extract event IDs from the timeline + timeline_event_ids = [event["event_id"] for event in timeline_events] + + # The sticky message event should NOT be in the timeline (filtered out) + self.assertNotIn( + sticky_event_id, + timeline_event_ids, + f"Sticky message event {sticky_event_id} should be filtered from timeline", + ) + + # The member event should be in the timeline + self.assertIn( + nonmessage_event_id, + timeline_event_ids, + f"Non-message event {nonmessage_event_id} should be in timeline", + ) + + # The sticky message event MUST appear in the sticky section + # because it was filtered from the timeline + received_sticky_event_ids = [e["event_id"] for e in sticky_events] + self.assertEqual( + received_sticky_event_ids, + [sticky_event_id], + ) + + def test_ignored_users_sticky_events(self) -> None: + """ + Test that sticky events from ignored users are not delivered to clients. + + > As with normal events, sticky events sent by ignored users MUST NOT be + > delivered to clients. + > — https://github.com/matrix-org/matrix-spec-proposals/blob/4340903c15e9eab1bfb2f6a31cfa08fd535f7e7c/proposals/4354-sticky-events.md#sync-api-changes + """ + # Register a second user who will be ignored + user2_id = self.register_user("user2", "pass") + user2_token = self.login(user2_id, "pass") + + # Join user2 to the room + self.helper.join(self.room_id, user2_id, tok=user2_token) + + # User1 ignores user2 + channel = self.make_request( + "PUT", + f"/_matrix/client/v3/user/{self.user_id}/account_data/m.ignored_user_list", + {"ignored_users": {user2_id: {}}}, + access_token=self.token, + ) + self.assertEqual(channel.code, 200, channel.result) + + # User2 sends a sticky event + sticky_response = self.helper.send_sticky_event( + self.room_id, + EventTypes.Message, + duration=Duration(minutes=1), + content={"body": "sticky from ignored user", "msgtype": "m.text"}, + tok=user2_token, + ) + sticky_event_id = sticky_response["event_id"] + + # User1 syncs + channel = self.make_request( + "GET", + "/sync", + access_token=self.token, + ) + self.assertEqual(channel.code, 200, channel.result) + + # Check timeline events - should not include the sticky event from ignored user + timeline_events = channel.json_body["rooms"]["join"][self.room_id]["timeline"][ + "events" + ] + timeline_event_ids = [event["event_id"] for event in timeline_events] + + self.assertNotIn( + sticky_event_id, + timeline_event_ids, + f"Sticky event from ignored user {sticky_event_id} should not be in timeline", + ) + + # Check sticky events section - should also not include the event + sticky_events = ( + channel.json_body["rooms"]["join"][self.room_id] + .get("msc4354_sticky", {}) + .get("events", []) + ) + + sticky_event_ids = [event["event_id"] for event in sticky_events] + + self.assertNotIn( + sticky_event_id, + sticky_event_ids, + f"Sticky event from ignored user {sticky_event_id} should not be in sticky section", + ) + + def test_history_visibility_bypass_for_sticky_events(self) -> None: + """ + Test that joined users can see sticky events even when history visibility + is set to "joined" and they joined after the event was sent. + This is required by MSC4354: "History visibility checks MUST NOT + be applied to sticky events. Any joined user is authorised to see + sticky events for the duration they remain sticky." + """ + # Create a new room with restricted history visibility + room_id = self.helper.create_room_as( + self.user_id, + tok=self.token, + extra_content={ + # Anyone can join + "preset": "public_chat", + # But you can't see history before you joined + "initial_state": [ + { + "type": EventTypes.RoomHistoryVisibility, + "state_key": "", + "content": {"history_visibility": "joined"}, + } + ], + }, + is_public=False, + ) + + # User1 sends a sticky event + sticky_event_id = self.helper.send_sticky_event( + room_id, + EventTypes.Message, + duration=Duration(minutes=5), + content={"body": "sticky message", "msgtype": "m.text"}, + tok=self.token, + )["event_id"] + + # User1 also sends a regular event, to verify our test setup + regular_event_id = self.helper.send( + room_id=room_id, + body="regular message", + tok=self.token, + )["event_id"] + + # Register and join a second user after the sticky event was sent + user2_id = self.register_user("user2", "pass") + user2_token = self.login(user2_id, "pass") + self.helper.join(room_id, user2_id, tok=user2_token) + + # User2 syncs - they should see the sticky event even though + # history visibility is "joined" and they joined after it was sent + channel = self.make_request( + "GET", + "/sync", + access_token=user2_token, + ) + self.assertEqual(channel.code, 200, channel.result) + + # Get sticky events from the sync response + sticky_events = channel.json_body["rooms"]["join"][room_id]["msc4354_sticky"][ + "events" + ] + sticky_event_ids = [event["event_id"] for event in sticky_events] + + # The sticky event should be visible to user2 + self.assertEqual( + [sticky_event_id], + sticky_event_ids, + f"Sticky event {sticky_event_id} should be visible despite history visibility", + ) + + # Also check that the regular (non-sticky) event sent at the same time + # is NOT visible. This is to verify our test setup. + timeline_events = channel.json_body["rooms"]["join"][room_id]["timeline"][ + "events" + ] + timeline_event_ids = [event["event_id"] for event in timeline_events] + + # The regular message should NOT be visible (history visibility = joined) + self.assertNotIn( + regular_event_id, + timeline_event_ids, + f"Expecting to not see regular event ({regular_event_id}) before user1 joined.", + ) + + @patch.object(StickyEvent, "MAX_EVENTS_IN_SYNC", 3) + def test_pagination_with_many_sticky_events(self) -> None: + """ + Test that pagination works correctly when there are more sticky events than + the intended limit. + + The MSC doesn't define a limit or how to set one. + See thread: https://github.com/matrix-org/matrix-spec-proposals/pull/4354#discussion_r2885670008 + + But Synapse currently emits 100 at a time, controlled by `MAX_EVENTS_IN_SYNC`. + In this test we patch it to 3 (as sending 100 events is not very efficient). + """ + + # A filter that excludes message events + # This is needed so that the sticky events come down the sticky section + # and not the timeline section, which would hamper our test. + filter_json = json.dumps( + { + "room": { + # We only want these io.element.example events + "timeline": {"types": ["io.element.example"]}, + } + } + ) + + # Send 8 sticky events: enough for 2 full pages and then a partial page with 2. + sent_sticky_event_ids = [] + for i in range(8): + response = self.helper.send_sticky_event( + self.room_id, + EventTypes.Message, + duration=Duration(minutes=1), + content={"body": f"sticky message {i}", "msgtype": "m.text"}, + tok=self.token, + ) + sent_sticky_event_ids.append(response["event_id"]) + + @dataclass + class SyncHelperResponse: + sticky_event_ids: list[str] + """Event IDs of events returned from the sticky section, in-order.""" + + next_batch: str + """Sync token to pass to `?since=` in order to do an incremental sync.""" + + def _do_sync(*, since: str | None) -> SyncHelperResponse: + """Small helper to do a sync and get the sticky events out.""" + and_since_param = "" if since is None else f"&since={since}" + channel = self.make_request( + "GET", + f"/sync?filter={quote(filter_json)}{and_since_param}", + access_token=self.token, + ) + self.assertEqual(channel.code, 200, channel.result) + + # Get sticky events from the sync response + sticky_events = [] + # In this test, when no sticky events are in the response, + # we don't have a rooms section at all + if "rooms" in channel.json_body: + sticky_events = channel.json_body["rooms"]["join"][self.room_id][ + "msc4354_sticky" + ]["events"] + + return SyncHelperResponse( + sticky_event_ids=[ + sticky_event["event_id"] for sticky_event in sticky_events + ], + next_batch=channel.json_body["next_batch"], + ) + + # Perform initial sync, we should get the first 3 sticky events, + # in order. + sync1 = _do_sync(since=None) + self.assertEqual(sync1.sticky_event_ids, sent_sticky_event_ids[0:3]) + + # Now do an incremental sync and expect the next page of 3 + sync2 = _do_sync(since=sync1.next_batch) + self.assertEqual(sync2.sticky_event_ids, sent_sticky_event_ids[3:6]) + + # Now do another incremental sync and expect the last 2 + sync3 = _do_sync(since=sync2.next_batch) + self.assertEqual(sync3.sticky_event_ids, sent_sticky_event_ids[6:8]) + + # Finally, expect an empty incremental sync at the end + sync4 = _do_sync(since=sync3.next_batch) + self.assertEqual(sync4.sticky_event_ids, []) From 86dc38621fe2744d5ceb723017dbab6d6b89553d Mon Sep 17 00:00:00 2001 From: Quentin Gliech Date: Tue, 10 Mar 2026 14:04:30 +0100 Subject: [PATCH 057/121] 1.149.0 --- CHANGES.md | 7 +++++++ debian/changelog | 6 ++++++ pyproject.toml | 2 +- 3 files changed, 14 insertions(+), 1 deletion(-) diff --git a/CHANGES.md b/CHANGES.md index 5996b7bcdfd..da4a1350d03 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,3 +1,10 @@ +# Synapse 1.149.0 (2026-03-10) + +No significant changes since 1.149.0rc1. + + + + # Synapse 1.149.0rc1 (2026-03-03) ## Features diff --git a/debian/changelog b/debian/changelog index 583bb35ad5b..b9ee8844941 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,3 +1,9 @@ +matrix-synapse-py3 (1.149.0) stable; urgency=medium + + * New synapse release 1.149.0. + + -- Synapse Packaging team Tue, 10 Mar 2026 13:02:53 +0000 + matrix-synapse-py3 (1.149.0~rc1) stable; urgency=medium * New synapse release 1.149.0rc1. diff --git a/pyproject.toml b/pyproject.toml index 9d5e3fec2d6..324bb3f15a7 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "matrix-synapse" -version = "1.149.0rc1" +version = "1.149.0" description = "Homeserver for the Matrix decentralised comms protocol" readme = "README.rst" authors = [ From f37a30d7c500a9df312a1da4dde78d0e945c03c2 Mon Sep 17 00:00:00 2001 From: Andrew Morgan <1342360+anoadragon453@users.noreply.github.com> Date: Wed, 11 Mar 2026 09:27:38 +0000 Subject: [PATCH 058/121] Bump `matrix-synapse-ldap3` to v0.4.0 in `poetry.lock` (#19543) To address https://github.com/element-hq/synapse/issues/19541 ### Pull Request Checklist * [x] Pull request is based on the develop branch * [x] Pull request includes a [changelog file](https://element-hq.github.io/synapse/latest/development/contributing_guide.html#changelog). The entry should: - Be a short description of your change which makes sense to users. "Fixed a bug that prevented receiving messages from other servers." instead of "Moved X method from `EventStore` to `EventWorkerStore`.". - Use markdown where necessary, mostly for `code blocks`. - End with either a period (.) or an exclamation mark (!). - Start with a capital letter. - Feel free to credit yourself, by adding a sentence "Contributed by @github_username." or "Contributed by [Your Name]." to the end of the entry. * [x] [Code style](https://element-hq.github.io/synapse/latest/code_style.html) is correct (run the [linters](https://element-hq.github.io/synapse/latest/development/contributing_guide.html#run-the-linters)) --- changelog.d/19543.misc | 1 + poetry.lock | 13 +++++++------ 2 files changed, 8 insertions(+), 6 deletions(-) create mode 100644 changelog.d/19543.misc diff --git a/changelog.d/19543.misc b/changelog.d/19543.misc new file mode 100644 index 00000000000..81ccf9515a6 --- /dev/null +++ b/changelog.d/19543.misc @@ -0,0 +1 @@ +Bump `matrix-synapse-ldap3` to `0.4.0` to support `setuptools>=82.0.0`. \ No newline at end of file diff --git a/poetry.lock b/poetry.lock index 7f9b34d38f8..a394c71bcaf 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1548,24 +1548,25 @@ test = ["aiounittest", "tox", "twisted"] [[package]] name = "matrix-synapse-ldap3" -version = "0.3.0" +version = "0.4.0" description = "An LDAP3 auth provider for Synapse" optional = true -python-versions = ">=3.7" +python-versions = ">=3.10" groups = ["main"] markers = "extra == \"matrix-synapse-ldap3\" or extra == \"all\"" files = [ - {file = "matrix-synapse-ldap3-0.3.0.tar.gz", hash = "sha256:8bb6517173164d4b9cc44f49de411d8cebdb2e705d5dd1ea1f38733c4a009e1d"}, - {file = "matrix_synapse_ldap3-0.3.0-py3-none-any.whl", hash = "sha256:8b4d701f8702551e98cc1d8c20dbed532de5613584c08d0df22de376ba99159d"}, + {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"}, ] [package.dependencies] ldap3 = ">=2.8" -service-identity = "*" +packaging = ">=14.1" +service_identity = "*" Twisted = ">=15.1.0" [package.extras] -dev = ["black (==22.3.0)", "flake8 (==4.0.1)", "isort (==5.9.3)", "ldaptor", "matrix-synapse", "mypy (==0.910)", "tox", "types-setuptools"] +dev = ["black (==24.2.0)", "flake8 (==7.0.0)", "isort (==5.9.3)", "ldaptor", "matrix-synapse", "mypy (==1.9.0)", "tox", "types-setuptools"] [[package]] name = "mdurl" From b99a58719b274fcbb327fd8d7649185792bfd12c Mon Sep 17 00:00:00 2001 From: Quentin Gliech Date: Wed, 11 Mar 2026 10:34:08 +0100 Subject: [PATCH 059/121] 1.149.1 --- CHANGES.md | 9 +++++++++ changelog.d/19543.misc | 1 - debian/changelog | 6 ++++++ pyproject.toml | 2 +- 4 files changed, 16 insertions(+), 2 deletions(-) delete mode 100644 changelog.d/19543.misc diff --git a/CHANGES.md b/CHANGES.md index da4a1350d03..1a5ff136c8a 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,3 +1,12 @@ +# Synapse 1.149.1 (2026-03-11) + +## Internal Changes + +- Bump `matrix-synapse-ldap3` to `0.4.0` to support `setuptools>=82.0.0`. Fixes [\#19541](https://github.com/element-hq/synapse/issues/19541). ([\#19543](https://github.com/element-hq/synapse/issues/19543)) + + + + # Synapse 1.149.0 (2026-03-10) No significant changes since 1.149.0rc1. diff --git a/changelog.d/19543.misc b/changelog.d/19543.misc deleted file mode 100644 index 81ccf9515a6..00000000000 --- a/changelog.d/19543.misc +++ /dev/null @@ -1 +0,0 @@ -Bump `matrix-synapse-ldap3` to `0.4.0` to support `setuptools>=82.0.0`. \ No newline at end of file diff --git a/debian/changelog b/debian/changelog index b9ee8844941..b0bc62684c8 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,3 +1,9 @@ +matrix-synapse-py3 (1.149.1) stable; urgency=medium + + * New synapse release 1.149.1. + + -- Synapse Packaging team Wed, 11 Mar 2026 09:30:24 +0000 + matrix-synapse-py3 (1.149.0) stable; urgency=medium * New synapse release 1.149.0. diff --git a/pyproject.toml b/pyproject.toml index 324bb3f15a7..ce87cb1492f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "matrix-synapse" -version = "1.149.0" +version = "1.149.1" description = "Homeserver for the Matrix decentralised comms protocol" readme = "README.rst" authors = [ From ae239280cb742968808ca1a4b7d7a84f67ca8177 Mon Sep 17 00:00:00 2001 From: Olivier 'reivilibre Date: Wed, 11 Mar 2026 15:38:45 +0000 Subject: [PATCH 060/121] Fix a bug introduced in v1.26.0 that caused deactivated, erased users to not be removed from the user directory. (#19542) Fixes: #19540 Fixes: #16290 (side effect of the proposed fix) Closes: #12804 (side effect of the proposed fix) Introduced in: https://github.com/matrix-org/synapse/pull/8932 --- This PR is a relatively simple simplification of the profile change on deactivation that appears to remove multiple bugs. This PR's **primary motivating fix** is #19540: when a user is deactivated and erased, they would be kept in the user directory. This bug appears to have been here since #8932 (previously https://github.com/matrix-org/synapse/pull/8932) (v1.26.0). The root cause of this bug is that after removing the user from the user directory, we would immediately update their displayname and avatar to empty strings (one at a time), which re-inserts the user into the user directory. With this PR, we now delete the entire `profiles` row upon user erasure, which is cleaner (from a 'your database goes back to zero after deactivating and erasing a user' point of view) and only needs one database operation (instead of doing displayname then avatar). With this PR, we also no longer send the 2 (deferred) `m.room.member` `join` events to every room to propagate the displayname and avatar_url changes. This is good for two reasons: - the user is about to get parted from those rooms anyway, so this reduces the number of state events sent per room from 3 to 1. (More efficient for us in the moment and leaves less litter in the room DAG.) - it is possible for the displayname/avatar update to be sent **after** the user parting, which seems as though it could trigger the user to be re-joined to a public room. (With that said, although this sounds vaguely familiar in my lossy memory, I can't find a ticket that actually describes this bug, so this might be fictional. Edit: #16290 seems to describe this, although the title is misleading.) Additionally, as a side effect of the proposed fix (deleting the `profiles` row), this PR also now deletes custom profile fields upon user erasure, which is a new feature/bugfix (not sure which) in its own right. I do not see a ticket that corresponds to this feature gap, possibly because custom profile fields are still a niche feature without mainstream support (to the best of my knowledge). Tests are included for the primary bugfix and for the cleanup of custom profile fields. ### `set_displayname` module API change This change includes a minor _technically_-breaking change to the module API. The change concerns `set_displayname` which is exposed to the module API with a `deactivation: bool = False` flag, matching the internal handler method it wraps. I suspect that this is a mistake caused by overly-faithfully piping through the args from the wrapped method (this Module API was introduced in https://github.com/matrix-org/synapse/pull/14629/changes#diff-0b449f6f95672437cf04f0b5512572b4a6a729d2759c438b7c206ea249619885R1592). The linked PR did the same for `by_admin` originally before it was changed. The `deactivation` flag's only purpose is to be piped through to other Module API callbacks when a module has registered to be notified about profile changes. My claim is that it makes no sense for the Module API to have this flag because it is not the one doing the deactivation, thus it should never be in a position to set this to `True`. My proposed change keeps the flag (for function signature compatibility), but turns it into a no-op (with a `ERROR` log when it's set to True by the module). The Module API callback notifying of the module-caused displayname change will therefore now always have `deactivation = False`. *Discussed in [`#synapse-dev:matrix.org`](https://matrix.to/#/!i5D5LLct_DYG-4hQprLzrxdbZ580U9UB6AEgFnk6rZQ/$1f8N6G_EJUI_I_LvplnVAF2UFZTw_FzgsPfB6pbcPKk?via=element.io&via=matrix.org&via=beeper.com)* --------- Signed-off-by: Olivier 'reivilibre --- changelog.d/19542.bugfix | 1 + docs/admin_api/user_admin_api.md | 1 + docs/upgrade.md | 13 +++ synapse/handlers/deactivate_account.py | 14 +-- synapse/handlers/profile.py | 108 +++++++++++++++++--- synapse/handlers/sso.py | 5 +- synapse/module_api/__init__.py | 25 ++++- synapse/rest/admin/users.py | 6 +- synapse/rest/client/profile.py | 12 +-- synapse/storage/databases/main/profile.py | 12 +++ tests/rest/client/test_third_party_rules.py | 6 +- tests/rest/synapse/mas/test_users.py | 77 +++++++++++++- 12 files changed, 245 insertions(+), 35 deletions(-) create mode 100644 changelog.d/19542.bugfix diff --git a/changelog.d/19542.bugfix b/changelog.d/19542.bugfix new file mode 100644 index 00000000000..ab72504335f --- /dev/null +++ b/changelog.d/19542.bugfix @@ -0,0 +1 @@ +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/docs/admin_api/user_admin_api.md b/docs/admin_api/user_admin_api.md index 9e0a1cb70ca..72e0e8d91a5 100644 --- a/docs/admin_api/user_admin_api.md +++ b/docs/admin_api/user_admin_api.md @@ -403,6 +403,7 @@ is set to `true`: - Remove the user's display name - Remove the user's avatar URL +- Remove the user's custom profile fields - Mark the user as erased The following actions are **NOT** performed. The list may be incomplete. diff --git a/docs/upgrade.md b/docs/upgrade.md index 0e2005f2820..777e57c4926 100644 --- a/docs/upgrade.md +++ b/docs/upgrade.md @@ -133,6 +133,19 @@ pip install systemd-python No action is needed if you do not use journal logging, or if you installed Synapse from the Debian packages (which handle this automatically). +## Module API: Deprecation of the `deactivation` parameter in the `set_displayname` method + +If you have Synapse modules installed that use the `set_displayname` method to change +the display name of your users, please ensure that it doesn't pass the optional +`deactivation` parameter. + +This parameter is now deprecated and it is intended to be removed in 2027. +No immediate change is necessary, however once the parameter is removed, modules passing it will produce errors. +[Issue #19546](https://github.com/element-hq/synapse/issues/19546) tracks this removal. + +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. + # Upgrading to v1.146.0 ## Drop support for Ubuntu 25.04 Plucky Puffin, and add support for 25.10 Questing Quokka diff --git a/synapse/handlers/deactivate_account.py b/synapse/handlers/deactivate_account.py index e4c646ce87e..538bdaaaf8e 100644 --- a/synapse/handlers/deactivate_account.py +++ b/synapse/handlers/deactivate_account.py @@ -167,13 +167,13 @@ async def deactivate_account( # Mark the user as erased, if they asked for that if erase_data: user = UserID.from_string(user_id) - # Remove avatar URL from this user - await self._profile_handler.set_avatar_url( - user, requester, "", by_admin, deactivation=True - ) - # Remove displayname from this user - await self._profile_handler.set_displayname( - user, requester, "", by_admin, deactivation=True + # Remove displayname, avatar URL and custom profile fields from this user + # + # Note that displayname and avatar URL may persist as historical state events + # in rooms, but these cases behave like message history, following + # https://spec.matrix.org/v1.17/client-server-api/#post_matrixclientv3accountdeactivate + await self._profile_handler.delete_profile_upon_deactivation( + user, requester, by_admin ) logger.info("Marking %s as erased", user_id) diff --git a/synapse/handlers/profile.py b/synapse/handlers/profile.py index 6bec901a792..d123bcdd367 100644 --- a/synapse/handlers/profile.py +++ b/synapse/handlers/profile.py @@ -35,6 +35,7 @@ SynapseError, ) from synapse.storage.databases.main.media_repository import LocalMedia, RemoteMedia +from synapse.storage.roommember import ProfileInfo from synapse.types import ( JsonDict, JsonMapping, @@ -193,18 +194,24 @@ async def set_displayname( target_user: UserID, requester: Requester, new_displayname: str, + *, by_admin: bool = False, - deactivation: bool = False, propagate: bool = True, ) -> None: """Set the displayname of a user + Preconditions: + - This must NOT be called as part of deactivating the user, because we will + notify modules about the change whilst claiming it is not related + to user deactivation and we will also (if `propagate=True`) send + updates into rooms, which could cause rooms to be accidentally joined + after the deactivated user has left them. + Args: target_user: the user whose displayname is to be changed. requester: The user attempting to make this change. new_displayname: The displayname to give this user. by_admin: Whether this change was made by an administrator. - deactivation: Whether this change was made while deactivating the user. propagate: Whether this change also applies to the user's membership events. """ if not self.hs.is_mine(target_user): @@ -248,12 +255,13 @@ async def set_displayname( await self.store.set_profile_displayname(target_user, displayname_to_set) profile = await self.store.get_profileinfo(target_user) + await self.user_directory_handler.handle_local_profile_change( target_user.to_string(), profile ) await self._third_party_rules.on_profile_update( - target_user.to_string(), profile, by_admin, deactivation + target_user.to_string(), profile, by_admin, deactivation=False ) if propagate: @@ -297,18 +305,24 @@ async def set_avatar_url( target_user: UserID, requester: Requester, new_avatar_url: str, + *, by_admin: bool = False, - deactivation: bool = False, propagate: bool = True, ) -> None: """Set a new avatar URL for a user. + Preconditions: + - This must NOT be called as part of deactivating the user, because we will + notify modules about the change whilst claiming it is not related + to user deactivation and we will also (if `propagate=True`) send + updates into rooms, which could cause rooms to be accidentally joined + after the deactivated user has left them. + Args: target_user: the user whose avatar URL is to be changed. requester: The user attempting to make this change. new_avatar_url: The avatar URL to give this user. by_admin: Whether this change was made by an administrator. - deactivation: Whether this change was made while deactivating the user. propagate: Whether this change also applies to the user's membership events. """ if not self.hs.is_mine(target_user): @@ -350,17 +364,79 @@ async def set_avatar_url( await self.store.set_profile_avatar_url(target_user, avatar_url_to_set) profile = await self.store.get_profileinfo(target_user) + await self.user_directory_handler.handle_local_profile_change( target_user.to_string(), profile ) await self._third_party_rules.on_profile_update( - target_user.to_string(), profile, by_admin, deactivation + target_user.to_string(), profile, by_admin, deactivation=False ) if propagate: await self._update_join_states(requester, target_user) + async def delete_profile_upon_deactivation( + self, + target_user: UserID, + requester: Requester, + by_admin: bool = False, + ) -> None: + """ + Clear the user's profile upon user deactivation (specifically, when user erasure is needed). + + This includes the displayname, avatar_url, all custom profile fields. + + The user directory is NOT updated in any way; it is the caller's responsibility to remove + the user from the user directory. + + Rooms' join states are NOT updated in any way; it is the caller's responsibility to soon + **leave** the room on the user's behalf, so there's no point sending new join events into + rooms to propagate the profile deletion. + See the `users_pending_deactivation` table and the associated user parter loop. + """ + if not self.hs.is_mine(target_user): + raise SynapseError(400, "User is not hosted on this homeserver") + + # Prevent users from deactivating anyone but themselves, + # except for admins who can deactivate anyone. + if not by_admin and target_user != requester.user: + # It's a little strange to have this check here, but given all the sibling + # methods have these checks, it'd be even stranger to be inconsistent and not + # have it. + raise AuthError(400, "Cannot remove another user's profile") + + if not by_admin: + current_profile = await self.store.get_profileinfo(target_user) + if not self.hs.config.registration.enable_set_displayname: + if current_profile.display_name: + # SUSPICIOUS: It seems strange to block deactivation on this, + # though this is preserving previous behaviour. + raise SynapseError( + 400, + "Changing display name is disabled on this server", + Codes.FORBIDDEN, + ) + + if not self.hs.config.registration.enable_set_avatar_url: + if current_profile.avatar_url: + # SUSPICIOUS: It seems strange to block deactivation on this, + # though this is preserving previous behaviour. + raise SynapseError( + 400, + "Changing avatar is disabled on this server", + Codes.FORBIDDEN, + ) + + await self.store.delete_profile(target_user) + + await self._third_party_rules.on_profile_update( + target_user.to_string(), + ProfileInfo(None, None), + by_admin, + deactivation=True, + ) + @cached() async def check_avatar_size_and_mime_type(self, mxc: str) -> bool: """Check that the size and content type of the avatar at the given MXC URI are @@ -482,18 +558,22 @@ async def set_profile_field( requester: Requester, field_name: str, new_value: JsonValue, + *, by_admin: bool = False, - deactivation: bool = False, ) -> None: """Set a new profile field for a user. + Preconditions: + - This must NOT be called as part of deactivating the user, because we will + notify modules about the change whilst claiming it is not related + to user deactivation. + Args: target_user: the user whose profile is to be changed. requester: The user attempting to make this change. field_name: The name of the profile field to update. new_value: The new field value for this user. by_admin: Whether this change was made by an administrator. - deactivation: Whether this change was made while deactivating the user. """ if not self.hs.is_mine(target_user): raise SynapseError(400, "User is not hosted on this homeserver") @@ -506,7 +586,7 @@ async def set_profile_field( # Custom fields do not propagate into the user directory *or* rooms. profile = await self.store.get_profileinfo(target_user) await self._third_party_rules.on_profile_update( - target_user.to_string(), profile, by_admin, deactivation + target_user.to_string(), profile, by_admin, deactivation=False ) async def delete_profile_field( @@ -514,17 +594,21 @@ async def delete_profile_field( target_user: UserID, requester: Requester, field_name: str, + *, by_admin: bool = False, - deactivation: bool = False, ) -> None: """Delete a field from a user's profile. + Preconditions: + - This must NOT be called as part of deactivating the user, because we will + notify modules about the change whilst claiming it is not related + to user deactivation. + Args: target_user: the user whose profile is to be changed. requester: The user attempting to make this change. field_name: The name of the profile field to remove. by_admin: Whether this change was made by an administrator. - deactivation: Whether this change was made while deactivating the user. """ if not self.hs.is_mine(target_user): raise SynapseError(400, "User is not hosted on this homeserver") @@ -537,7 +621,7 @@ async def delete_profile_field( # Custom fields do not propagate into the user directory *or* rooms. profile = await self.store.get_profileinfo(target_user) await self._third_party_rules.on_profile_update( - target_user.to_string(), profile, by_admin, deactivation + target_user.to_string(), profile, by_admin, deactivation=False ) async def on_profile_query(self, args: JsonDict) -> JsonDict: diff --git a/synapse/handlers/sso.py b/synapse/handlers/sso.py index ebbe7afa849..0b2587f94fc 100644 --- a/synapse/handlers/sso.py +++ b/synapse/handlers/sso.py @@ -522,7 +522,10 @@ async def complete_sso_login_request( authenticated_entity=user_id, ) await self._profile_handler.set_displayname( - user_id_obj, requester, attributes.display_name, True + user_id_obj, + requester, + attributes.display_name, + by_admin=True, ) if attributes.picture: await self.set_avatar(user_id, attributes.picture) diff --git a/synapse/module_api/__init__.py b/synapse/module_api/__init__.py index 0580f3665cf..947be24d3e3 100644 --- a/synapse/module_api/__init__.py +++ b/synapse/module_api/__init__.py @@ -160,6 +160,7 @@ from synapse.util.clock import Clock from synapse.util.duration import Duration from synapse.util.frozenutils import freeze +from synapse.util.sentinel import Sentinel if TYPE_CHECKING: # Old versions don't have `LiteralString` @@ -1989,27 +1990,47 @@ async def set_displayname( self, user_id: UserID, new_displayname: str, - deactivation: bool = False, + deactivation: bool | Sentinel = Sentinel.UNSET_SENTINEL, ) -> None: """Sets a user's display name. Added in Synapse v1.76.0. + (Synapse Developer note: All future arguments should be kwargs-only + due to https://github.com/element-hq/synapse/issues/19546) + Args: user_id: The user whose display name is to be changed. new_displayname: The new display name to give the user. deactivation: + **deprecated since v1.150.0** + Callers should NOT pass this argument. Instead, omit it and leave it to the default. + Will log an error if it is passed. + Remove after 2027-01-01 + Tracked by https://github.com/element-hq/synapse/issues/19546 + Whether this change was made while deactivating the user. + + Should be omitted, will produce a logged error if set to True. + It's likely that this flag should have stayed internal-only and + was accidentally exposed to the Module API. + It no longer has any function. """ requester = create_requester(user_id) + + if deactivation is not Sentinel.UNSET_SENTINEL: + logger.error( + "Deprecated `deactivation` parameter passed to `set_displayname` Module API (value: %r). This will break in 2027.", + deactivation, + ) + await self._hs.get_profile_handler().set_displayname( target_user=user_id, requester=requester, new_displayname=new_displayname, by_admin=True, - deactivation=deactivation, ) def get_current_time_msec(self) -> int: diff --git a/synapse/rest/admin/users.py b/synapse/rest/admin/users.py index 807a9cad5b2..8265c2d789a 100644 --- a/synapse/rest/admin/users.py +++ b/synapse/rest/admin/users.py @@ -369,7 +369,7 @@ async def on_PUT( if user: # modify user if "displayname" in body: await self.profile_handler.set_displayname( - target_user, requester, body["displayname"], True + target_user, requester, body["displayname"], by_admin=True ) if threepids is not None: @@ -418,7 +418,7 @@ async def on_PUT( if "avatar_url" in body: await self.profile_handler.set_avatar_url( - target_user, requester, body["avatar_url"], True + target_user, requester, body["avatar_url"], by_admin=True ) if "admin" in body: @@ -526,7 +526,7 @@ async def on_PUT( if "avatar_url" in body and isinstance(body["avatar_url"], str): await self.profile_handler.set_avatar_url( - target_user, requester, body["avatar_url"], True + target_user, requester, body["avatar_url"], by_admin=True ) user_info_dict = await self.admin_handler.get_user(target_user) diff --git a/synapse/rest/client/profile.py b/synapse/rest/client/profile.py index 7f3128cb617..c2ec5b36114 100644 --- a/synapse/rest/client/profile.py +++ b/synapse/rest/client/profile.py @@ -206,15 +206,15 @@ async def on_PUT( if field_name == ProfileFields.DISPLAYNAME: await self.profile_handler.set_displayname( - user, requester, new_value, is_admin, propagate=propagate + user, requester, new_value, by_admin=is_admin, propagate=propagate ) elif field_name == ProfileFields.AVATAR_URL: await self.profile_handler.set_avatar_url( - user, requester, new_value, is_admin, propagate=propagate + user, requester, new_value, by_admin=is_admin, propagate=propagate ) else: await self.profile_handler.set_profile_field( - user, requester, field_name, new_value, is_admin + user, requester, field_name, new_value, by_admin=is_admin ) return 200, {} @@ -263,15 +263,15 @@ async def on_DELETE( if field_name == ProfileFields.DISPLAYNAME: await self.profile_handler.set_displayname( - user, requester, "", is_admin, propagate=propagate + user, requester, "", by_admin=is_admin, propagate=propagate ) elif field_name == ProfileFields.AVATAR_URL: await self.profile_handler.set_avatar_url( - user, requester, "", is_admin, propagate=propagate + user, requester, "", by_admin=is_admin, propagate=propagate ) else: await self.profile_handler.delete_profile_field( - user, requester, field_name, is_admin + user, requester, field_name, by_admin=is_admin ) return 200, {} diff --git a/synapse/storage/databases/main/profile.py b/synapse/storage/databases/main/profile.py index 11ad516eb34..9b787e19a3d 100644 --- a/synapse/storage/databases/main/profile.py +++ b/synapse/storage/databases/main/profile.py @@ -534,6 +534,18 @@ def delete_profile_field(txn: LoggingTransaction) -> None: await self.db_pool.runInteraction("delete_profile_field", delete_profile_field) + async def delete_profile(self, user_id: UserID) -> None: + """ + Deletes an entire user profile, including displayname, avatar_url and all custom fields. + Used at user deactivation when erasure is requested. + """ + + await self.db_pool.simple_delete( + desc="delete_profile", + table="profiles", + keyvalues={"full_user_id": user_id.to_string()}, + ) + class ProfileStore(ProfileWorkerStore): pass diff --git a/tests/rest/client/test_third_party_rules.py b/tests/rest/client/test_third_party_rules.py index 0d319dff7ee..1709b27f67c 100644 --- a/tests/rest/client/test_third_party_rules.py +++ b/tests/rest/client/test_third_party_rules.py @@ -734,9 +734,9 @@ def test_on_user_deactivation_status_changed(self) -> None: self.assertTrue(args[1]) self.assertFalse(args[2]) - # Check that the profile update callback was called twice (once for the display - # name and once for the avatar URL), and that the "deactivation" boolean is true. - self.assertEqual(profile_mock.call_count, 2) + # Check that the profile update callback was called once + # and that the "deactivation" boolean is true. + self.assertEqual(profile_mock.call_count, 1) args = profile_mock.call_args[0] self.assertTrue(args[3]) diff --git a/tests/rest/synapse/mas/test_users.py b/tests/rest/synapse/mas/test_users.py index 4e8cf907006..f0f26a939c9 100644 --- a/tests/rest/synapse/mas/test_users.py +++ b/tests/rest/synapse/mas/test_users.py @@ -10,11 +10,14 @@ # # See the GNU Affero General Public License for more details: # . - +from http import HTTPStatus from urllib.parse import urlencode +from parameterized import parameterized + from twisted.internet.testing import MemoryReactor +from synapse.api.errors import StoreError from synapse.appservice import ApplicationService from synapse.server import HomeServer from synapse.types import JsonDict, UserID, create_requester @@ -621,6 +624,78 @@ def test_delete_user_erase(self) -> None: # And erased self.assertTrue(self.get_success(store.is_user_erased(user_id=str(alice)))) + @parameterized.expand([(False,), (True,)]) + def test_user_deactivation_removes_user_from_user_directory( + self, erase: bool + ) -> None: + """ + Test that when `/delete_user` deactivates a user, they get removed from the user directory. + + This is applicable regardless of whether the `erase` flag is set. + + Regression test for: + 'Users are not removed from user directory upon deactivation if erase flag is true' + https://github.com/element-hq/synapse/issues/19540 + """ + alice = UserID("alice", "test") + store = self.hs.get_datastores().main + + # Ensure we're testing what we think we are: + # check the user is IN the directory at the start of the test + self.assertEqual( + self.get_success(store._get_user_in_directory(alice.to_string())), + ("Alice", None), + ) + + channel = self.make_request( + "POST", + "/_synapse/mas/delete_user", + shorthand=False, + access_token=self.SHARED_SECRET, + content={"localpart": "alice", "erase": erase}, + ) + self.assertEqual(channel.code, 200, channel.json_body) + self.assertEqual(channel.json_body, {}) + + self.assertIsNone( + self.get_success(store._get_user_in_directory(alice.to_string())), + "User still present in user directory after deactivation!", + ) + + def test_user_deactivation_removes_custom_profile_fields(self) -> None: + """ + Test that when `/delete_user` deactivates a user with the `erase` flag, + their custom profile fields get removed. + """ + alice = UserID("alice", "test") + store = self.hs.get_datastores().main + + # Add custom profile field + self.get_success(store.set_profile_field(alice, "io.element.example", "hello")) + + # Ensure we're testing what we think we are: + # check the user has profile data at the start of the test + self.assertEqual( + self.get_success(store.get_profile_fields(alice)), + {"io.element.example": "hello"}, + ) + + channel = self.make_request( + "POST", + "/_synapse/mas/delete_user", + shorthand=False, + access_token=self.SHARED_SECRET, + content={"localpart": "alice", "erase": True}, + ) + self.assertEqual(channel.code, 200, channel.json_body) + self.assertEqual(channel.json_body, {}) + + # With no profile, we expect a 404 (Not Found) StoreError + with self.assertRaises(StoreError) as raised: + self.get_success_or_raise(store.get_profile_fields(alice)) + + self.assertEqual(raised.exception.code, HTTPStatus.NOT_FOUND) + def test_delete_user_missing_localpart(self) -> None: channel = self.make_request( "POST", From e30001883c30f38818d8ef6239145f9b42f89dd7 Mon Sep 17 00:00:00 2001 From: Eric Eastwood Date: Wed, 11 Mar 2026 15:30:32 -0500 Subject: [PATCH 061/121] Add in-repo Complement test to sanity check Synapse version matches git checkout (#19476) This way we actually detect problems like https://github.com/element-hq/synapse/pull/19475 as they happen instead of being invisible until something breaks. Sanity check that Complement is testing against your code changes (whether it be local or from the PR in CI). ``` COMPLEMENT_DIR=../complement ./scripts-dev/complement.sh --in-repo -run TestSynapseVersion ``` --- .github/workflows/tests.yml | 33 ++++++ changelog.d/19476.misc | 1 + complement/go.mod | 1 + complement/go.sum | 12 +++ .../tests/synapse_version_check_test.go | 101 ++++++++++++++++++ docker/Dockerfile | 8 ++ scripts-dev/complement.sh | 6 ++ synapse/util/__init__.py | 13 ++- 8 files changed, 172 insertions(+), 3 deletions(-) create mode 100644 changelog.d/19476.misc create mode 100644 complement/tests/synapse_version_check_test.go diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 98b47130a1c..b39c453959f 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -705,6 +705,13 @@ jobs: toolchain: ${{ env.RUST_VERSION }} - uses: Swatinem/rust-cache@779680da715d629ac1d338a641029a2f4372abb5 # v2.8.2 + # We use `poetry` in `complement.sh` + - uses: matrix-org/setup-python-poetry@5bbf6603c5c930615ec8a29f1b5d7d258d905aa4 # v2.0.0 + with: + poetry-version: "2.1.1" + # Matches the `path` where we checkout Synapse above + working-directory: "synapse" + - name: Prepare Complement's Prerequisites run: synapse/.ci/scripts/setup_complement_prerequisites.sh @@ -713,6 +720,32 @@ jobs: cache-dependency-path: complement/go.sum go-version-file: complement/go.mod + # Run the image sanity check test first as this is the first thing we want to know + # about (are we actually testing what we expect?) and we don't want to debug + # downstream failures (wild goose chase). + - name: Sanity check Complement image + id: run_sanity_check_complement_image_test + # -p=1: We're using `-p 1` to force the test packages to run serially as GHA boxes + # are underpowered and don't like running tons of Synapse instances at once. + # -json: Output JSON format so that gotestfmt can parse it. + # + # tee /tmp/gotest-complement.log: We tee the output to a file so that we can re-process it + # later on for better formatting with gotestfmt. But we still want the command + # to output to the terminal as it runs so we can see what's happening in + # real-time. + run: | + set -o pipefail + COMPLEMENT_DIR=`pwd`/complement synapse/scripts-dev/complement.sh --in-repo -p 1 -json -run 'TestSynapseVersion/Synapse_version_matches_current_git_checkout' 2>&1 | tee /tmp/gotest-sanity-check-complement.log + shell: bash + env: + POSTGRES: ${{ (matrix.database == 'Postgres') && 1 || '' }} + WORKERS: ${{ (matrix.arrangement == 'workers') && 1 || '' }} + + - name: Formatted sanity check Complement test logs + # Always run this step if we attempted to run the Complement tests. + if: always() && steps.run_sanity_check_complement_image_test.outcome != 'skipped' + run: cat /tmp/gotest-sanity-check-complement.log | gotestfmt -hide "successful-downloads,empty-packages" + - name: Run Complement Tests id: run_complement_tests # -p=1: We're using `-p 1` to force the test packages to run serially as GHA boxes diff --git a/changelog.d/19476.misc b/changelog.d/19476.misc new file mode 100644 index 00000000000..c1869911a47 --- /dev/null +++ b/changelog.d/19476.misc @@ -0,0 +1 @@ +Add in-repo Complement test to sanity check Synapse version matches git checkout (testing what we think we are). diff --git a/complement/go.mod b/complement/go.mod index c6c1678bacc..2a9adb9b955 100644 --- a/complement/go.mod +++ b/complement/go.mod @@ -31,6 +31,7 @@ require ( github.com/go-logr/logr v1.4.3 // indirect github.com/go-logr/stdr v1.2.2 // indirect github.com/gogo/protobuf v1.3.2 // indirect + github.com/matrix-org/gomatrix v0.0.0-20220926102614-ceba4d9f7530 // indirect github.com/matrix-org/util v0.0.0-20221111132719-399730281e66 // indirect github.com/moby/docker-image-spec v1.3.1 // indirect github.com/moby/term v0.5.2 // indirect diff --git a/complement/go.sum b/complement/go.sum index c674730c057..79a35aa14c6 100644 --- a/complement/go.sum +++ b/complement/go.sum @@ -38,6 +38,8 @@ github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.3 h1:5ZPtiqj0JL5oKWmcsq4VMaAW5ukBEgSGXEN89zeH1Jo= github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.3/go.mod h1:ndYquD05frm2vACXE1nsccT4oJzjhw2arTS2cpUD1PI= +github.com/h2non/parth v0.0.0-20190131123155-b4df798d6542 h1:2VTzZjLZBgl62/EtslCrtky5vbi9dd7HrQPQIx6wqiw= +github.com/h2non/parth v0.0.0-20190131123155-b4df798d6542/go.mod h1:Ow0tF8D4Kplbc8s8sSb3V2oUCygFHVp8gC3Dn6U4MNI= github.com/hashicorp/go-set/v3 v3.0.0 h1:CaJBQvQCOWoftrBcDt7Nwgo0kdpmrKxar/x2o6pV9JA= github.com/hashicorp/go-set/v3 v3.0.0/go.mod h1:IEghM2MpE5IaNvL+D7X480dfNtxjRXZ6VMpK3C8s2ok= github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= @@ -50,6 +52,8 @@ github.com/matrix-org/gomatrixserverlib v0.0.0-20250813150445-9f5070a65744 h1:5G github.com/matrix-org/gomatrixserverlib v0.0.0-20250813150445-9f5070a65744/go.mod h1:b6KVfDjXjA5Q7vhpOaMqIhFYvu5BuFVZixlNeTV/CLc= github.com/matrix-org/util v0.0.0-20221111132719-399730281e66 h1:6z4KxomXSIGWqhHcfzExgkH3Z3UkIXry4ibJS4Aqz2Y= github.com/matrix-org/util v0.0.0-20221111132719-399730281e66/go.mod h1:iBI1foelCqA09JJgPV0FYz4qA5dUXYOxMi57FxKBdd4= +github.com/miekg/dns v1.1.66 h1:FeZXOS3VCVsKnEAd+wBkjMC3D2K+ww66Cq3VnCINuJE= +github.com/miekg/dns v1.1.66/go.mod h1:jGFzBsSNbJw6z1HYut1RKBKHA9PBdxeHrZG8J+gC2WE= github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0= github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo= github.com/moby/sys/atomicwriter v0.1.0 h1:kw5D/EqkBwsBFi0ss9v1VG3wIkVhzGvLklJ+w3A14Sw= @@ -120,6 +124,8 @@ golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 h1:2dVuKD2vS7b0QIHQbpyTISPd0 golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56/go.mod h1:M4RDyNAINzryxdtnbRXRL/OHtkFuWGRjvuhBJpk2IlY= golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.24.0 h1:ZfthKaKaT4NrhGVZHO1/WDTwGES4De8KtWO0SIbNJMU= +golang.org/x/mod v0.24.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= @@ -129,6 +135,8 @@ golang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.14.0 h1:woo0S4Yywslg6hp4eUFjTVOyKt0RookbpAHG4c1HmhQ= +golang.org/x/sync v0.14.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -147,6 +155,8 @@ golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtn golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0= +golang.org/x/tools v0.33.0 h1:4qz2S3zmRxbGIhDIAgjxvFutSvH5EfnsYrRBj0UI0bc= +golang.org/x/tools v0.33.0/go.mod h1:CIJMaWEY88juyUfo7UbgPqbC8rU2OqfAV1h2Qp0oMYI= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= @@ -160,6 +170,8 @@ google.golang.org/grpc v1.72.2/go.mod h1:wH5Aktxcg25y1I3w7H69nHfXdOG3UiadoBtjh3i google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY= google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/h2non/gock.v1 v1.1.2 h1:jBbHXgGBK/AoPVfJh5x4r/WxIrElvbLel8TCZkkZJoY= +gopkg.in/h2non/gock.v1 v1.1.2/go.mod h1:n7UGz/ckNChHiK05rDoiC4MYSunEC/lyaUm2WWaDva0= gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/complement/tests/synapse_version_check_test.go b/complement/tests/synapse_version_check_test.go new file mode 100644 index 00000000000..790a6a40ce9 --- /dev/null +++ b/complement/tests/synapse_version_check_test.go @@ -0,0 +1,101 @@ +// This file is licensed under the Affero General Public License (AGPL) version 3. +// +// Copyright (C) 2026 Element Creations Ltd +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as +// published by the Free Software Foundation, either version 3 of the +// License, or (at your option) any later version. +// +// See the GNU Affero General Public License for more details: +// . + +package synapse_tests + +import ( + "net/http" + "os/exec" + "strings" + "testing" + + "github.com/matrix-org/complement" + "github.com/matrix-org/complement/match" + "github.com/matrix-org/complement/must" + "github.com/tidwall/gjson" +) + +func TestSynapseVersion(t *testing.T) { + deployment := complement.Deploy(t, 1) + defer deployment.Destroy(t) + + unauthedClient := deployment.UnauthenticatedClient(t, "hs1") + + // Sanity check that the version of Synapse used in the `COMPLEMENT_BASE_IMAGE` + // matches the same git commit we have checked out. This ensures that the image being + // used in Complement is the one that we just built locally with `complement.sh` + // instead of accidentally pulling in some remote one. + // + // This test is expected to pass if you use `complement.sh`. + // + // If this test fails, it probably means that Complement is using an image that + // doesn't encompass the changes you have checked out (unexpected). We want to yell + // loudly and point out what's wrong instead of silently letting your PRs pass + // without actually being tested. + t.Run("Synapse version matches current git checkout", func(t *testing.T) { + // Get the Synapse version details of the current git checkout + checkoutSynapseVersion := runCommand( + t, + []string{ + "poetry", + "run", + "python", + "-c", + "from synapse.util import SYNAPSE_VERSION; print(SYNAPSE_VERSION)", + }, + ) + + // Find the version details of the Synapse instance deployed from the Docker image + res := unauthedClient.MustDo(t, "GET", []string{"_matrix", "federation", "v1", "version"}) + body := must.MatchResponse(t, res, match.HTTPResponse{ + StatusCode: http.StatusOK, + JSON: []match.JSON{ + match.JSONKeyPresent("server"), + }, + }) + rawSynapseVersionString := gjson.GetBytes(body, "server.version").Str + t.Logf( + "Synapse version string from federation version endpoint: %s", + rawSynapseVersionString, + ) + + must.Equal( + t, + rawSynapseVersionString, + checkoutSynapseVersion, + "Synapse version in the checkout doesn't match the Synapse version that Complement is running. "+ + "If this test fails, it probably means that Complement is using an image that "+ + "doesn't encompass the changes you have checked out (unexpected). We want to yell "+ + "loudly and point out what's wrong instead of silently letting your PRs pass "+ + "without actually being tested.", + ) + }) +} + +// runCommand will run the given command and return the stdout (whitespace +// trimmed). +func runCommand(t *testing.T, commandPieces []string) string { + t.Helper() + + // Then run our actual command + cmd := exec.Command(commandPieces[0], commandPieces[1:]...) + output, err := cmd.Output() + if err != nil { + t.Fatalf( + "runCommand: failed to run command (%s): %v", + strings.Join(commandPieces, " "), + err, + ) + } + + return strings.TrimSpace(string(output)) +} diff --git a/docker/Dockerfile b/docker/Dockerfile index 9f74e48df35..59771ae88fe 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -171,6 +171,14 @@ FROM docker.io/library/python:${PYTHON_VERSION}-slim-${DEBIAN_VERSION} ARG TARGETARCH +# If specified, Synapse will use this as the version string in the app. +# +# This can be useful to capture the git info of the build as `.git/` won't be +# available in the Docker image for Synapse to generate from. +ARG SYNAPSE_VERSION_STRING +# Pass it through to Synapse as an environment variable. +ENV SYNAPSE_VERSION_STRING=${SYNAPSE_VERSION_STRING} + LABEL org.opencontainers.image.url='https://github.com/element-hq/synapse' LABEL org.opencontainers.image.documentation='https://element-hq.github.io/synapse/latest/' LABEL org.opencontainers.image.source='https://github.com/element-hq/synapse.git' diff --git a/scripts-dev/complement.sh b/scripts-dev/complement.sh index fec005fdb13..c65ae53df0d 100755 --- a/scripts-dev/complement.sh +++ b/scripts-dev/complement.sh @@ -215,11 +215,17 @@ main() { $CONTAINER_RUNTIME run --rm -v $editable_mount --entrypoint 'cp' "$COMPLEMENT_SYNAPSE_EDITABLE_IMAGE_PATH" -- /synapse_rust.abi3.so.bak /editable-src/synapse/synapse_rust.abi3.so else + # We remove the `egg-info` as it can contain outdated information which won't line + # up with our current reality. + rm -rf matrix_synapse.egg-info/ + # Figure out the Synapse version string in our current checkout + synapse_version_string="$(poetry run python -c 'from synapse.util import SYNAPSE_VERSION; print(SYNAPSE_VERSION)')" # Build the base Synapse image from the local checkout echo_if_github "::group::Build Docker image: matrixdotorg/synapse" $CONTAINER_RUNTIME build \ -t "$SYNAPSE_IMAGE_PATH" \ + --build-arg SYNAPSE_VERSION_STRING="$synapse_version_string" \ --build-arg TEST_ONLY_SKIP_DEP_HASH_VERIFICATION \ --build-arg TEST_ONLY_IGNORE_POETRY_LOCKFILE \ -f "docker/Dockerfile" . diff --git a/synapse/util/__init__.py b/synapse/util/__init__.py index fbd01914d56..f2898de0b8e 100644 --- a/synapse/util/__init__.py +++ b/synapse/util/__init__.py @@ -21,6 +21,7 @@ import collections.abc import logging +import os import typing from typing import ( Iterator, @@ -74,9 +75,15 @@ def log_failure( return None -# Version string with git info. Computed here once so that we don't invoke git multiple -# times. -SYNAPSE_VERSION = get_distribution_version_string("matrix-synapse", __file__) +SYNAPSE_VERSION = os.getenv( + "SYNAPSE_VERSION_STRING" +) or get_distribution_version_string("matrix-synapse", __file__) +""" +Version string with git info. + +This can be overridden via the `SYNAPSE_VERSION_STRING` environment variable or is +computed here once so that we don't invoke git multiple times. +""" class ExceptionBundle(Exception): From 18f717d7170de9864d564a5b8c13c33855e575cc Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 12 Mar 2026 13:41:06 +0000 Subject: [PATCH 062/121] Bump tornado from 6.5.4 to 6.5.5 (#19551) Bumps [tornado](https://github.com/tornadoweb/tornado) from 6.5.4 to 6.5.5.
Changelog

Sourced from tornado's changelog.

Release notes

.. toctree:: :maxdepth: 2

releases/v6.5.5 releases/v6.5.4 releases/v6.5.3 releases/v6.5.2 releases/v6.5.1 releases/v6.5.0 releases/v6.4.2 releases/v6.4.1 releases/v6.4.0 releases/v6.3.3 releases/v6.3.2 releases/v6.3.1 releases/v6.3.0 releases/v6.2.0 releases/v6.1.0 releases/v6.0.4 releases/v6.0.3 releases/v6.0.2 releases/v6.0.1 releases/v6.0.0 releases/v5.1.1 releases/v5.1.0 releases/v5.0.2 releases/v5.0.1 releases/v5.0.0 releases/v4.5.3 releases/v4.5.2 releases/v4.5.1 releases/v4.5.0 releases/v4.4.3 releases/v4.4.2 releases/v4.4.1 releases/v4.4.0 releases/v4.3.0 releases/v4.2.1 releases/v4.2.0 releases/v4.1.0 releases/v4.0.2 releases/v4.0.1 releases/v4.0.0 releases/v3.2.2 releases/v3.2.1 releases/v3.2.0 releases/v3.1.1

... (truncated)

Commits
  • 7d64650 Merge pull request #3586 from bdarnell/update-cibw
  • d05d59b build: Bump cibuildwheel to 3.4.0
  • c2f4673 Merge pull request #3585 from bdarnell/release-655
  • e5f1aa4 Release notes and version bump for v6.5.5
  • 78a046f httputil: Add CRLF to _FORBIDDEN_HEADER_CHARS_RE
  • 24a2d96 web: Validate characters in all cookie attributes.
  • 119a195 httputil: Add limits on multipart form data parsing
  • See full diff in compare view

[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=tornado&package-manager=pip&previous-version=6.5.4&new-version=6.5.5)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores) Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`. [//]: # (dependabot-automerge-start) [//]: # (dependabot-automerge-end) ---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot show ignore conditions` will show all of the ignore conditions of the specified dependency - `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself) You can disable automated security fix PRs for this repo from the [Security Alerts page](https://github.com/element-hq/synapse/network/alerts).
Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- poetry.lock | 68 ++++++++++++++++++++++++++--------------------------- 1 file changed, 33 insertions(+), 35 deletions(-) diff --git a/poetry.lock b/poetry.lock index 0d03c860f5f..f5088ea9b9f 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"}, ] @@ -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"}, @@ -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"}, From 8d03a4df11aa122e39c0698b6a9b7e0d3b9694d7 Mon Sep 17 00:00:00 2001 From: Quentin Gliech Date: Thu, 12 Mar 2026 15:17:13 +0100 Subject: [PATCH 063/121] Avoid re-computing the event ID when cloning events. (#19527) `event_id` is a lazily-computed property on events, as it's a hash of the event content on room version 3 and later. The reason we do this is that it helps finding database inconsistencies by not trusting the event ID we got from the database. The thing is, when we clone events (to return them through /sync or /messages for example) we don't copy the computed hash if we already computed it, duplicating the work. This copies the internal `_event_id` property. --- changelog.d/19527.misc | 1 + synapse/events/utils.py | 10 +++++++++- 2 files changed, 10 insertions(+), 1 deletion(-) create mode 100644 changelog.d/19527.misc diff --git a/changelog.d/19527.misc b/changelog.d/19527.misc new file mode 100644 index 00000000000..c349af286b1 --- /dev/null +++ b/changelog.d/19527.misc @@ -0,0 +1 @@ +Avoid re-computing the event ID when cloning events. diff --git a/synapse/events/utils.py b/synapse/events/utils.py index b79a68f589a..1bf4d632c00 100644 --- a/synapse/events/utils.py +++ b/synapse/events/utils.py @@ -48,7 +48,7 @@ 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 From 3ce5508c7e1a7277987e368f1fce4804d51a887c Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 12 Mar 2026 15:44:21 +0100 Subject: [PATCH 064/121] Bump quinn-proto from 0.11.12 to 0.11.14 (#19544) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps [quinn-proto](https://github.com/quinn-rs/quinn) from 0.11.12 to 0.11.14.
Release notes

Sourced from quinn-proto's releases.

quinn-proto 0.11.14

@​jxs reported a denial of service issue in quinn-proto 5 days ago:

We coordinated with them to release this version to patch the issue. Unfortunately the maintainers missed these issues during code review and we did not have enough fuzzing coverage -- we regret the oversight and have added an additional fuzzing target.

Organizations that want to participate in coordinated disclosure can contact us privately to discuss terms.

What's Changed

Commits
  • 2c315aa proto: bump version to 0.11.14
  • 8ad47f4 Use newer rustls-pki-types PEM parser API
  • c81c028 ci: fix workflow syntax
  • 0050172 ci: pin wasm-bindgen-cli version
  • 8a6f82c Take semver-compatible dependency updates
  • e52db4a Apply suggestions from clippy 1.91
  • 6df7275 chore: Fix unnecessary_unwrap clippy
  • c8eefa0 proto: avoid unwrapping varint decoding during parameters parsing
  • 9723a97 fuzz: add fuzzing target for parsing transport parameters
  • eaf0ef3 Fix over-permissive proto dependency edge (#2385)
  • Additional commits viewable in compare view

[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=quinn-proto&package-manager=cargo&previous-version=0.11.12&new-version=0.11.14)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores) Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`. [//]: # (dependabot-automerge-start) [//]: # (dependabot-automerge-end) ---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot show ignore conditions` will show all of the ignore conditions of the specified dependency - `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself) You can disable automated security fix PRs for this repo from the [Security Alerts page](https://github.com/element-hq/synapse/network/alerts).
Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- Cargo.lock | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 340114f8018..eed90d64714 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -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", From 4c475dcd7a566367708c59642d17cf7fdd3c0507 Mon Sep 17 00:00:00 2001 From: Quentin Gliech Date: Thu, 12 Mar 2026 18:11:09 +0100 Subject: [PATCH 065/121] Allow the caching of the /versions and /auth_metadata endpoints (#19530) Can be reviewed commit by commit. This sets caching headers on the /versions and /auth_metadata endpoints to: - allow clients to cache the response for up to 10 minutes (`max-age=600`) - allow proxies to cache the response for up to an hour (`s-maxage=3600`) - make proxies serve stale response for up to an hour (`s-maxage=3600`) but make them refresh their response after 10 minutes (`stale-while-revalidate=600`) so that we always have a snappy response to client, but also have fresh responses most of the time - only cache the response for unauthenticated requests on /versions (`Vary: Authorization`) I'm not too worried about the 1h TTL on the proxy side, as with the `stale-while-revalidate` directive, one just needs to do two requests after 10 minutes to get a fresh response from the cache. The reason we want this, is that clients usually load this right away, leading to a lot of traffic from people just loading the Element Web login screen with the default config. This is currently routed to `client_readers` on matrix.org (and ESS) which can be overwhelmed for other reasons, leading to slow response times on those endpoints (3s+). Overwhelmed workers shouldn't prevent people from logging in, and shouldn't result in a long loading spinner in clients. This PR allows caching proxies (like Cloudflare) to publicly cache the unauthenticated response of those two endpoints and make it load quicker, reducing server load as well. --- changelog.d/19530.misc | 1 + synapse/http/server.py | 26 +++++++++++++++++-- synapse/rest/client/auth_metadata.py | 38 ++++++++++++++++++++++++++++ synapse/rest/client/versions.py | 20 +++++++++++++++ 4 files changed, 83 insertions(+), 2 deletions(-) create mode 100644 changelog.d/19530.misc diff --git a/changelog.d/19530.misc b/changelog.d/19530.misc new file mode 100644 index 00000000000..9e5bc0fe047 --- /dev/null +++ b/changelog.d/19530.misc @@ -0,0 +1 @@ +Allow caching of the `/versions` and `/auth_metadata` public endpoints. diff --git a/synapse/http/server.py b/synapse/http/server.py index 226cb008317..2c235e04f4e 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/rest/client/auth_metadata.py b/synapse/rest/client/auth_metadata.py index 702f550906b..062b8ed13e3 100644 --- a/synapse/rest/client/auth_metadata.py +++ b/synapse/rest/client/auth_metadata.py @@ -49,6 +49,25 @@ def __init__(self, hs: "HomeServer"): 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 @@ def __init__(self, hs: "HomeServer"): 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/versions.py b/synapse/rest/client/versions.py index 23f5ffeedb3..f8d7a1a4d9d 100644 --- a/synapse/rest/client/versions.py +++ b/synapse/rest/client/versions.py @@ -81,6 +81,26 @@ async def on_GET(self, request: SynapseRequest) -> tuple[int, JsonDict]: 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, From c0924fbbd8ba6c3d9c0984a05a3160494abc7fbd Mon Sep 17 00:00:00 2001 From: Andrew Ferrazzutti Date: Mon, 16 Mar 2026 12:29:42 -0400 Subject: [PATCH 066/121] MSC4140: put delay_id in unsigned data for sender (#19479) Implements https://github.com/matrix-org/matrix-spec-proposals/pull/4140/changes/49b200dcc11de286974925177b1e184cd905e6fa --- changelog.d/19479.feature | 1 + rust/src/events/internal_metadata.rs | 22 +++++ synapse/events/utils.py | 81 +++++++++-------- synapse/handlers/delayed_events.py | 2 + synapse/handlers/message.py | 13 ++- synapse/handlers/room_member.py | 10 +++ synapse/synapse_rust/events.pyi | 2 + tests/rest/client/test_delayed_events.py | 108 ++++++++++++++++++++++- 8 files changed, 199 insertions(+), 40 deletions(-) create mode 100644 changelog.d/19479.feature diff --git a/changelog.d/19479.feature b/changelog.d/19479.feature new file mode 100644 index 00000000000..3e7e8bd6ffb --- /dev/null +++ b/changelog.d/19479.feature @@ -0,0 +1 @@ +[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. diff --git a/rust/src/events/internal_metadata.rs b/rust/src/events/internal_metadata.rs index fa40fdcfad8..595f9cf7eb7 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/synapse/events/utils.py b/synapse/events/utils.py index 1bf4d632c00..89eb2182afd 100644 --- a/synapse/events/utils.py +++ b/synapse/events/utils.py @@ -420,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 @@ -483,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 - - 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 + # 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 ) - 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 7e41716f1e2..4a9f646d4db 100644 --- a/synapse/handlers/delayed_events.py +++ b/synapse/handlers/delayed_events.py @@ -560,6 +560,7 @@ async def _send_event( 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 @@ async def _send_event( 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/message.py b/synapse/handlers/message.py index 99ce120736d..eb016225159 100644 --- a/synapse/handlers/message.py +++ b/synapse/handlers/message.py @@ -585,6 +585,7 @@ async def create_event( 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 @@ async def create_event( 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 @@ async def create_event( 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 @@ async def create_event( 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 @@ async def create_and_send_nonmember_event( 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 @@ async def create_and_send_nonmember_event( 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 @@ async def create_and_send_nonmember_event( 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 @@ async def _create_and_send_nonmember_event_locked( 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 @@ async def _create_and_send_nonmember_event_locked( 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 0c6be727169..b2e678e90e9 100644 --- a/synapse/handlers/room_member.py +++ b/synapse/handlers/room_member.py @@ -408,6 +408,7 @@ async def _local_membership_update( 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 @@ async def _local_membership_update( 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 @@ async def _local_membership_update( 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 @@ async def update_membership( 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 @@ async def update_membership( 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 @@ async def update_membership( state_event_ids=state_event_ids, depth=depth, origin_server_ts=origin_server_ts, + delay_id=delay_id, ) return result @@ -701,6 +707,7 @@ async def update_membership_locked( 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 @@ async def update_membership_locked( 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 @@ async def update_membership_locked( 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 @@ async def update_membership_locked( 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/synapse_rust/events.pyi b/synapse/synapse_rust/events.pyi index 0add391c651..185f29694b0 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 efa69a393ad..da904ce1f51 100644 --- a/tests/rest/client/test_delayed_events.py +++ b/tests/rest/client/test_delayed_events.py @@ -22,7 +22,7 @@ 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 @@ def test_delayed_state_events_are_sent_on_timeout(self) -> None: 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 @@ def test_delayed_state_events_are_sent_on_timeout(self) -> None: ) 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 @@ def test_cancel_delayed_state_event(self, action_in_path: bool) -> None: 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 @@ def test_send_delayed_state_event( ) 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 @@ def test_restart_delayed_state_event(self, action_in_path: bool) -> None: ) 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 @@ def test_delayed_state_is_not_cancelled_by_new_state_from_same_user( 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 @@ def test_delayed_state_is_not_cancelled_by_new_state_from_same_user( ) 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 @@ def test_delayed_state_is_cancelled_by_new_state_from_other_user( 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 @@ def test_delayed_state_is_cancelled_by_new_state_from_other_user( ) 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 @@ def _update_delayed_event( 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 From eedd4c8796b7abdbeea5d6622e0c45c3056f5fe9 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 16 Mar 2026 18:30:42 +0100 Subject: [PATCH 067/121] Bump pyjwt from 2.11.0 to 2.12.0 (#19560) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps [pyjwt](https://github.com/jpadilla/pyjwt) from 2.11.0 to 2.12.0.
Release notes

Sourced from pyjwt's releases.

2.12.0

Security

What's Changed

New Contributors

Full Changelog: https://github.com/jpadilla/pyjwt/compare/2.11.0...2.12.0

Changelog

Sourced from pyjwt's changelog.

v2.12.0 <https://github.com/jpadilla/pyjwt/compare/2.11.0...2.12.0>__

Fixed


- Annotate PyJWKSet.keys for pyright by @tamird in
`[#1134](https://github.com/jpadilla/pyjwt/issues/1134)
<https://github.com/jpadilla/pyjwt/pull/1134>`__
- Close ``HTTPError`` response to prevent ``ResourceWarning`` on Python
3.14 by @veeceey in
`[#1133](https://github.com/jpadilla/pyjwt/issues/1133)
<https://github.com/jpadilla/pyjwt/pull/1133>`__
- Do not keep ``algorithms`` dict in PyJWK instances by @akx in
`[#1143](https://github.com/jpadilla/pyjwt/issues/1143)
<https://github.com/jpadilla/pyjwt/pull/1143>`__
- Validate the crit (Critical) Header Parameter defined in RFC 7515
§4.1.11. by @dmbs335 in `GHSA-752w-5fwx-jx9f
<https://github.com/jpadilla/pyjwt/security/advisories/GHSA-752w-5fwx-jx9f>`__
- Use PyJWK algorithm when encoding without explicit algorithm in
`[#1148](https://github.com/jpadilla/pyjwt/issues/1148)
<https://github.com/jpadilla/pyjwt/pull/1148>`__

Added

  • Docs: Add PyJWKClient API reference and document the two-tier caching system (JWK Set cache and signing key LRU cache).
Commits

[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=pyjwt&package-manager=pip&previous-version=2.11.0&new-version=2.12.0)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores) Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`. [//]: # (dependabot-automerge-start) [//]: # (dependabot-automerge-end) ---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot show ignore conditions` will show all of the ignore conditions of the specified dependency - `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself) You can disable automated security fix PRs for this repo from the [Security Alerts page](https://github.com/element-hq/synapse/network/alerts).
Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- poetry.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/poetry.lock b/poetry.lock index f5088ea9b9f..0bcb88a0469 100644 --- a/poetry.lock +++ b/poetry.lock @@ -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] From cdd261b1c60b96fbe690e8bc0aa9dd6c76cb5530 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 16 Mar 2026 17:35:35 +0000 Subject: [PATCH 068/121] Bump pyopenssl from 25.3.0 to 26.0.0 (#19574) Bumps [pyopenssl](https://github.com/pyca/pyopenssl) from 25.3.0 to 26.0.0.
Changelog

Sourced from pyopenssl's changelog.

26.0.0 (2026-03-15)

Backward-incompatible changes: ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

  • Dropped support for Python 3.7.
  • The minimum cryptography version is now 46.0.0.

Deprecations: ^^^^^^^^^^^^^

Changes: ^^^^^^^^

  • Added support for using aws-lc instead of OpenSSL.
  • Properly raise an error if a DTLS cookie callback returned a cookie longer than DTLS1_COOKIE_LENGTH bytes. Previously this would result in a buffer-overflow. Credit to dark_haxor for reporting the issue. CVE-2026-27459
  • Added OpenSSL.SSL.Connection.get_group_name to determine which group name was negotiated.
  • Context.set_tlsext_servername_callback now handles exceptions raised in the callback by calling sys.excepthook and returning a fatal TLS alert. Previously, exceptions were silently swallowed and the handshake would proceed as if the callback had succeeded. Credit to Leury Castillo for reporting this issue. CVE-2026-27448
Commits
  • 358cbf2 Prepare for 26.0.0 release (#1487)
  • a8d28e7 Bump actions/cache from 4 to 5 (#1486)
  • 6fefff0 Add aws-lc compatibility to tests and CI (#1476)
  • a739f96 Bump actions/download-artifact from 8.0.0 to 8.0.1 (#1485)
  • 8b4c66b Bump actions/upload-artifact in /.github/actions/upload-coverage (#1484)
  • 02a5c78 Bump actions/upload-artifact from 6.0.0 to 7.0.0 (#1483)
  • d973387 Bump actions/download-artifact from 7.0.0 to 8.0.0 (#1482)
  • 57f09bb Fix buffer overflow in DTLS cookie generation callback (#1479)
  • d41a814 Handle exceptions in set_tlsext_servername_callback callbacks (#1478)
  • 7b29beb Fix not using a cryptography wheel on uv (#1475)
  • Additional commits viewable in compare view

[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=pyopenssl&package-manager=pip&previous-version=25.3.0&new-version=26.0.0)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores) Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`. [//]: # (dependabot-automerge-start) [//]: # (dependabot-automerge-end) ---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot show ignore conditions` will show all of the ignore conditions of the specified dependency - `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself) You can disable automated security fix PRs for this repo from the [Security Alerts page](https://github.com/element-hq/synapse/network/alerts).
Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- poetry.lock | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/poetry.lock b/poetry.lock index 0bcb88a0469..15cc023f228 100644 --- a/poetry.lock +++ b/poetry.lock @@ -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] From a71c468b04f308809b489e230edf3a1e91f23d0c Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 16 Mar 2026 17:52:13 +0000 Subject: [PATCH 069/121] Bump the patches group with 2 updates (#19536) Bumps the patches group with 2 updates: [anyhow](https://github.com/dtolnay/anyhow) and [pyo3-log](https://github.com/vorner/pyo3-log). Updates `anyhow` from 1.0.101 to 1.0.102
Release notes

Sourced from anyhow's releases.

1.0.102

Commits
  • 5c657b3 Release 1.0.102
  • e737fb6 Merge pull request #442 from dtolnay/backtrace
  • 7fe62b5 Further simply backtrace conditional compilation
  • c8cb5ca Merge pull request #441 from dtolnay/backtrace
  • de27df7 Delete CI use of --features=backtrace
  • 9b67e5d Merge pull request #440 from dtolnay/backtrace
  • efdb11a Simplify std_backtrace conditional code
  • b8a9a70 Merge pull request #439 from dtolnay/backtrace
  • a42fc2c Remove feature = "backtrace" conditional code
  • 2a2a3ce Re-word backtrace feature comment
  • Additional commits viewable in compare view

Updates `pyo3-log` from 0.13.2 to 0.13.3
Changelog

Sourced from pyo3-log's changelog.

0.13.3

  • Support for pyo3 0.28 (#75).
Commits

Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`. [//]: # (dependabot-automerge-start) [//]: # (dependabot-automerge-end) ---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot show ignore conditions` will show all of the ignore conditions of the specified dependency - `@dependabot ignore major version` will close this group update PR and stop Dependabot creating any more for the specific dependency's major version (unless you unignore this specific dependency's major version or upgrade to it yourself) - `@dependabot ignore minor version` will close this group update PR and stop Dependabot creating any more for the specific dependency's minor version (unless you unignore this specific dependency's minor version or upgrade to it yourself) - `@dependabot ignore ` will close this group update PR and stop Dependabot creating any more for the specific dependency (unless you unignore this specific dependency or upgrade to it yourself) - `@dependabot unignore ` will remove all of the ignore conditions of the specified dependency - `@dependabot unignore ` will remove the ignore condition of the specified dependency and ignore conditions
Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- Cargo.lock | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index eed90d64714..d6945bfbb79 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", From 3aa948c50c1f0b1d6e87ea7f222cc155dea77e3b Mon Sep 17 00:00:00 2001 From: Olivier 'reivilibre Date: Mon, 16 Mar 2026 18:27:54 +0000 Subject: [PATCH 070/121] When Matrix Authentication Service (MAS) integration is enabled, allow MAS to set the user locked status in Synapse. (#19554) Companion PR: https://github.com/element-hq/matrix-authentication-service/pull/5550 to 1) send this flag and 2) provision users proactively when their lock status changes. --- Currently Synapse and MAS have two independent user lock implementations. This PR makes it so that MAS can push its lock status to Synapse when 'provisioning' the user. Having the lock status in Synapse is useful for removing users from the user directory when they are locked. There is otherwise no authentication requirement to have it in Synapse; the enforcement is done by MAS at token introspection time. --------- Signed-off-by: Olivier 'reivilibre --- changelog.d/19554.feature | 1 + synapse/rest/synapse/mas/users.py | 15 +++++++ tests/rest/synapse/mas/test_users.py | 58 ++++++++++++++++++++++++++++ 3 files changed, 74 insertions(+) create mode 100644 changelog.d/19554.feature diff --git a/changelog.d/19554.feature b/changelog.d/19554.feature new file mode 100644 index 00000000000..d30d3da1f6c --- /dev/null +++ b/changelog.d/19554.feature @@ -0,0 +1 @@ +When Matrix Authentication Service (MAS) integration is enabled, allow MAS to set the user locked status in Synapse. \ No newline at end of file diff --git a/synapse/rest/synapse/mas/users.py b/synapse/rest/synapse/mas/users.py index 55c73375551..01db41bcfac 100644 --- a/synapse/rest/synapse/mas/users.py +++ b/synapse/rest/synapse/mas/users.py @@ -112,6 +112,18 @@ class PostBody(RequestBodyModel): 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 @@ async def _async_render_POST( 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/tests/rest/synapse/mas/test_users.py b/tests/rest/synapse/mas/test_users.py index f0f26a939c9..6f44761bb8b 100644 --- a/tests/rest/synapse/mas/test_users.py +++ b/tests/rest/synapse/mas/test_users.py @@ -362,6 +362,64 @@ def test_provision_user_invalid_json_types(self) -> None: ) 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): From 6254e009bbcf5991a1490645a77a5bd4addbbb80 Mon Sep 17 00:00:00 2001 From: Eric Eastwood Date: Mon, 16 Mar 2026 21:56:16 -0500 Subject: [PATCH 071/121] Fix `Build and push complement image` CI job pointing to non-existent image (#19523) :x: https://github.com/element-hq/synapse/actions/runs/22609655282/job/65509315002#step:8:39 ``` Error response from daemon: No such image: complement-synapse:latest ``` Regressed in https://github.com/element-hq/synapse/pull/19475#discussion_r2823157623 where we updated `complement.sh` to build `localhost/complement-synapse` instead of `complement-synapse`. --- .github/workflows/push_complement_image.yml | 1 + changelog.d/19523.bugfix | 1 + scripts-dev/complement.sh | 13 +++++++++---- 3 files changed, 11 insertions(+), 4 deletions(-) create mode 100644 changelog.d/19523.bugfix diff --git a/.github/workflows/push_complement_image.yml b/.github/workflows/push_complement_image.yml index 12b4720ca5a..40ff3ba0e0e 100644 --- a/.github/workflows/push_complement_image.yml +++ b/.github/workflows/push_complement_image.yml @@ -69,6 +69,7 @@ jobs: run: | for TAG in ${{ join(fromJson(steps.meta.outputs.json).tags, ' ') }}; do echo "tag and push $TAG" + # `localhost/complement-synapse` should match the image created by `scripts-dev/complement.sh` docker tag complement-synapse $TAG docker push $TAG done diff --git a/changelog.d/19523.bugfix b/changelog.d/19523.bugfix new file mode 100644 index 00000000000..e9f53c61bab --- /dev/null +++ b/changelog.d/19523.bugfix @@ -0,0 +1 @@ +Fix `Build and push complement image` CI job pointing to non-existent image. diff --git a/scripts-dev/complement.sh b/scripts-dev/complement.sh index c65ae53df0d..a8a361fd4a6 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" From c37a5bb4cdb8894bb87b2229992faa4daa29ee33 Mon Sep 17 00:00:00 2001 From: Eric Eastwood Date: Mon, 16 Mar 2026 22:20:56 -0500 Subject: [PATCH 072/121] Restore `localhost/complement-synapse` change from #19523 See https://github.com/element-hq/synapse/pull/19523#discussion_r2944133700 --- .github/workflows/push_complement_image.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/push_complement_image.yml b/.github/workflows/push_complement_image.yml index 40ff3ba0e0e..7793172e559 100644 --- a/.github/workflows/push_complement_image.yml +++ b/.github/workflows/push_complement_image.yml @@ -70,6 +70,6 @@ jobs: for TAG in ${{ join(fromJson(steps.meta.outputs.json).tags, ' ') }}; do echo "tag and push $TAG" # `localhost/complement-synapse` should match the image created by `scripts-dev/complement.sh` - docker tag complement-synapse $TAG + docker tag localhost/complement-synapse $TAG docker push $TAG done From 8a6d9a8d4550c3b0f6232ef5a2a5bfa51fbc5534 Mon Sep 17 00:00:00 2001 From: Andrew Ferrazzutti Date: Tue, 17 Mar 2026 10:36:07 -0400 Subject: [PATCH 073/121] Admin API docs: use consistent path param syntax (#19307) Always use `/` instead of sometimes using `/$param` ### Pull Request Checklist * [x] Pull request is based on the develop branch * [x] Pull request includes a [changelog file](https://element-hq.github.io/synapse/latest/development/contributing_guide.html#changelog). The entry should: - Be a short description of your change which makes sense to users. "Fixed a bug that prevented receiving messages from other servers." instead of "Moved X method from `EventStore` to `EventWorkerStore`.". - Use markdown where necessary, mostly for `code blocks`. - End with either a period (.) or an exclamation mark (!). - Start with a capital letter. - Feel free to credit yourself, by adding a sentence "Contributed by @github_username." or "Contributed by [Your Name]." to the end of the entry. * [x] [Code style](https://element-hq.github.io/synapse/latest/code_style.html) is correct (run the [linters](https://element-hq.github.io/synapse/latest/development/contributing_guide.html#run-the-linters)) --- changelog.d/19307.doc | 1 + docs/admin_api/user_admin_api.md | 12 ++++++------ 2 files changed, 7 insertions(+), 6 deletions(-) create mode 100644 changelog.d/19307.doc diff --git a/changelog.d/19307.doc b/changelog.d/19307.doc new file mode 100644 index 00000000000..a27e97f3d05 --- /dev/null +++ b/changelog.d/19307.doc @@ -0,0 +1 @@ +In the Admin API documentation, always express path parameters as `/` instead of as `/$param`. diff --git a/docs/admin_api/user_admin_api.md b/docs/admin_api/user_admin_api.md index 72e0e8d91a5..14f86fa9768 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: From 8ad7e8af81bd655deb2014a7df04cf218c8a829d Mon Sep 17 00:00:00 2001 From: Eric Eastwood Date: Tue, 17 Mar 2026 09:43:05 -0500 Subject: [PATCH 074/121] Add some light labels to the `Processed request` logs (#19548) It's pretty hard to remember the order of all of these ambiguous numbers. I assume they're not totally labeled already to cut down on the length when scanning with your eyes. This just adds a few hints of what each grouping is. Spawning from [staring at some Synapse logs](https://github.com/element-hq/matrix-hosted/issues/10631) and cross-referencing the Synapse source code over and over. --- changelog.d/19548.misc | 1 + docs/upgrade.md | 8 ++++++++ docs/usage/administration/request_log.md | 4 ++-- synapse/http/site.py | 4 +++- 4 files changed, 14 insertions(+), 3 deletions(-) create mode 100644 changelog.d/19548.misc diff --git a/changelog.d/19548.misc b/changelog.d/19548.misc new file mode 100644 index 00000000000..eed4d5d8e2c --- /dev/null +++ b/changelog.d/19548.misc @@ -0,0 +1 @@ +Add a few labels to the number groupings in the `Processed request` logs. diff --git a/docs/upgrade.md b/docs/upgrade.md index 777e57c4926..aeae82c114a 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 6154108934f..7e3047eb62e 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/synapse/http/site.py b/synapse/http/site.py index 6ced5b98b3c..9b7fd5c936e 100644 --- a/synapse/http/site.py +++ b/synapse/http/site.py @@ -638,10 +638,12 @@ def _finished_processing(self) -> None: 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, From 6a63f0dcd7e59df6acceb9ee8049325a99481c5b Mon Sep 17 00:00:00 2001 From: Quentin Gliech Date: Tue, 17 Mar 2026 15:45:28 +0100 Subject: [PATCH 075/121] Migrate dev dependencies to PEP 735 dependency groups (#19490) This moves the dev dependencies to PEP 735 dependency groups, to help us move to standard project metadata, which will help us moving to `uv` (#19566) This requires poetry 2.2.0 --- .github/workflows/fix_lint.yaml | 2 +- .github/workflows/latest_deps.yml | 2 +- .github/workflows/tests.yml | 18 +++--- .github/workflows/twisted_trunk.yml | 4 +- changelog.d/19490.misc | 1 + debian/build_virtualenv | 2 +- debian/changelog | 1 + docker/Dockerfile | 2 +- docs/development/dependencies.md | 2 +- mypy.ini | 2 +- poetry.lock | 2 +- pyproject.toml | 93 +++++++++++++++-------------- 12 files changed, 68 insertions(+), 63 deletions(-) create mode 100644 changelog.d/19490.misc diff --git a/.github/workflows/fix_lint.yaml b/.github/workflows/fix_lint.yaml index babc3bc5de1..4752b6afebd 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 5bc78062cda..2d945c2096b 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/tests.yml b/.github/workflows/tests.yml index b39c453959f..a03d27472d7 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 @@ -708,7 +708,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" diff --git a/.github/workflows/twisted_trunk.yml b/.github/workflows/twisted_trunk.yml index 12fdbbe7c47..d38e38ebcb5 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/changelog.d/19490.misc b/changelog.d/19490.misc new file mode 100644 index 00000000000..924197a1cbc --- /dev/null +++ b/changelog.d/19490.misc @@ -0,0 +1 @@ +Migrate `dev` dependencies to [PEP 735](https://peps.python.org/pep-0735/) dependency groups. diff --git a/debian/build_virtualenv b/debian/build_virtualenv index 70d4efcbd0d..7bbf52ddd97 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 9cd39ee76c0..1a21a105b87 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,6 +1,7 @@ matrix-synapse-py3 (1.149.1+nmu1) UNRELEASED; urgency=medium * 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 diff --git a/docker/Dockerfile b/docker/Dockerfile index 59771ae88fe..6070d5c3558 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/development/dependencies.md b/docs/development/dependencies.md index 1b3348703f4..fe0667194a8 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/mypy.ini b/mypy.ini index d6a34342933..ec73ce9f6e7 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 15cc023f228..681a183b422 100644 --- a/poetry.lock +++ b/poetry.lock @@ -3754,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 07ab1ee5dbf..f655a1ef8be 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -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" - -# 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" - -# 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 schema delta files -sqlglot = ">=28.0.0" +[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", + + # 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", + + # Used for checking the Poetry lockfile + "tomli>=1.2.3", + + # Used for checking the schema delta files + "sqlglot>=28.0.0", +] [tool.towncrier] package = "synapse" From 7d8e8747eade7314f083ff4c9d9ef2d0becb5124 Mon Sep 17 00:00:00 2001 From: Quentin Gliech Date: Tue, 17 Mar 2026 15:56:55 +0100 Subject: [PATCH 076/121] 1.150.0rc1 --- CHANGES.md | 33 +++++++++++++++++++++++++++++++ changelog.d/19307.doc | 1 - changelog.d/19311.misc | 1 - changelog.d/19314.feature | 1 - changelog.d/19476.misc | 1 - changelog.d/19479.feature | 1 - changelog.d/19487.feature | 1 - changelog.d/19490.misc | 1 - changelog.d/19491.misc | 1 - changelog.d/19517.doc | 1 - changelog.d/19518.doc | 1 - changelog.d/19523.bugfix | 1 - changelog.d/19527.misc | 1 - changelog.d/19530.misc | 1 - changelog.d/19542.bugfix | 1 - changelog.d/19548.misc | 1 - changelog.d/19554.feature | 1 - debian/changelog | 8 ++++++-- pyproject.toml | 2 +- schema/synapse-config.schema.yaml | 2 +- 20 files changed, 41 insertions(+), 20 deletions(-) delete mode 100644 changelog.d/19307.doc delete mode 100644 changelog.d/19311.misc delete mode 100644 changelog.d/19314.feature delete mode 100644 changelog.d/19476.misc delete mode 100644 changelog.d/19479.feature delete mode 100644 changelog.d/19487.feature delete mode 100644 changelog.d/19490.misc delete mode 100644 changelog.d/19491.misc delete mode 100644 changelog.d/19517.doc delete mode 100644 changelog.d/19518.doc delete mode 100644 changelog.d/19523.bugfix delete mode 100644 changelog.d/19527.misc delete mode 100644 changelog.d/19530.misc delete mode 100644 changelog.d/19542.bugfix delete mode 100644 changelog.d/19548.misc delete mode 100644 changelog.d/19554.feature diff --git a/CHANGES.md b/CHANGES.md index 1a5ff136c8a..e7c24162385 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/changelog.d/19307.doc b/changelog.d/19307.doc deleted file mode 100644 index a27e97f3d05..00000000000 --- a/changelog.d/19307.doc +++ /dev/null @@ -1 +0,0 @@ -In the Admin API documentation, always express path parameters as `/` instead of as `/$param`. diff --git a/changelog.d/19311.misc b/changelog.d/19311.misc deleted file mode 100644 index 66ec86c02d0..00000000000 --- 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 fd2893c577d..00000000000 --- 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/19476.misc b/changelog.d/19476.misc deleted file mode 100644 index c1869911a47..00000000000 --- 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/19479.feature b/changelog.d/19479.feature deleted file mode 100644 index 3e7e8bd6ffb..00000000000 --- a/changelog.d/19479.feature +++ /dev/null @@ -1 +0,0 @@ -[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. diff --git a/changelog.d/19487.feature b/changelog.d/19487.feature deleted file mode 100644 index 4eb1d8f2617..00000000000 --- 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/19490.misc b/changelog.d/19490.misc deleted file mode 100644 index 924197a1cbc..00000000000 --- a/changelog.d/19490.misc +++ /dev/null @@ -1 +0,0 @@ -Migrate `dev` dependencies to [PEP 735](https://peps.python.org/pep-0735/) dependency groups. diff --git a/changelog.d/19491.misc b/changelog.d/19491.misc deleted file mode 100644 index 62b0ddddc2c..00000000000 --- 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 778c14c6aac..00000000000 --- 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 5de867c8d7b..00000000000 --- a/changelog.d/19518.doc +++ /dev/null @@ -1 +0,0 @@ -Unify Complement developer docs. diff --git a/changelog.d/19523.bugfix b/changelog.d/19523.bugfix deleted file mode 100644 index e9f53c61bab..00000000000 --- a/changelog.d/19523.bugfix +++ /dev/null @@ -1 +0,0 @@ -Fix `Build and push complement image` CI job pointing to non-existent image. diff --git a/changelog.d/19527.misc b/changelog.d/19527.misc deleted file mode 100644 index c349af286b1..00000000000 --- a/changelog.d/19527.misc +++ /dev/null @@ -1 +0,0 @@ -Avoid re-computing the event ID when cloning events. diff --git a/changelog.d/19530.misc b/changelog.d/19530.misc deleted file mode 100644 index 9e5bc0fe047..00000000000 --- a/changelog.d/19530.misc +++ /dev/null @@ -1 +0,0 @@ -Allow caching of the `/versions` and `/auth_metadata` public endpoints. diff --git a/changelog.d/19542.bugfix b/changelog.d/19542.bugfix deleted file mode 100644 index ab72504335f..00000000000 --- 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/19548.misc b/changelog.d/19548.misc deleted file mode 100644 index eed4d5d8e2c..00000000000 --- a/changelog.d/19548.misc +++ /dev/null @@ -1 +0,0 @@ -Add a few labels to the number groupings in the `Processed request` logs. diff --git a/changelog.d/19554.feature b/changelog.d/19554.feature deleted file mode 100644 index d30d3da1f6c..00000000000 --- a/changelog.d/19554.feature +++ /dev/null @@ -1 +0,0 @@ -When Matrix Authentication Service (MAS) integration is enabled, allow MAS to set the user locked status in Synapse. \ No newline at end of file diff --git a/debian/changelog b/debian/changelog index 1a21a105b87..55af4c22b55 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,9 +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/pyproject.toml b/pyproject.toml index f655a1ef8be..adb9993aae8 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 = [ diff --git a/schema/synapse-config.schema.yaml b/schema/synapse-config.schema.yaml index 3a61a4c6fc9..dbf7d7acb7d 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: From d65ef848eb14f6e0c69e2a9614776a814d330244 Mon Sep 17 00:00:00 2001 From: Eric Eastwood Date: Tue, 17 Mar 2026 10:51:53 -0500 Subject: [PATCH 077/121] Fix `Build and push complement image` CI job not having Poetry for `complement.sh` (#19578) :x: `Build and push complement image`, https://github.com/element-hq/synapse/actions/runs/23176317296/job/67339146082 ``` scripts-dev/complement.sh: line 227: poetry: command not found ``` Follow-up to https://github.com/element-hq/synapse/pull/19523 This regressed in https://github.com/element-hq/synapse/pull/19476 ### Testing strategy 1. Visit https://github.com/element-hq/synapse/actions/workflows/push_complement_image.yml 1. **Run workflow**: - **Use workflow from:** `madlittlemods/fix-complement-push-image-ci-job-poetry` - **Branch:** `develop` 1. Wait for CI to run and pass :white_check_mark: --- .github/workflows/push_complement_image.yml | 4 ++++ changelog.d/19578.bugfix | 1 + 2 files changed, 5 insertions(+) create mode 100644 changelog.d/19578.bugfix diff --git a/.github/workflows/push_complement_image.yml b/.github/workflows/push_complement_image.yml index 7793172e559..12599457dc7 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: diff --git a/changelog.d/19578.bugfix b/changelog.d/19578.bugfix new file mode 100644 index 00000000000..dbaf4be7e89 --- /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. From 0d4accb0a6915e97384e0dcebe9c96c975a09f38 Mon Sep 17 00:00:00 2001 From: Olivier 'reivilibre Date: Tue, 17 Mar 2026 17:08:04 +0000 Subject: [PATCH 078/121] Remove support for MSC3852: Expose user agent information on Device as the MSC was closed. (#19430) Fixes: #14836 Discovered whilst looking at the state of MSCs in Synapse. --------- Signed-off-by: Olivier 'reivilibre --- changelog.d/19430.removal | 1 + synapse/config/experimental.py | 3 --- synapse/handlers/device.py | 1 - synapse/rest/client/devices.py | 24 ++++-------------------- 4 files changed, 5 insertions(+), 24 deletions(-) create mode 100644 changelog.d/19430.removal diff --git a/changelog.d/19430.removal b/changelog.d/19430.removal new file mode 100644 index 00000000000..8920bccc38a --- /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/synapse/config/experimental.py b/synapse/config/experimental.py index a8c9305704e..41efff1d8f0 100644 --- a/synapse/config/experimental.py +++ b/synapse/config/experimental.py @@ -445,9 +445,6 @@ def read_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/handlers/device.py b/synapse/handlers/device.py index 29074f5e20c..9a371651fb0 100644 --- a/synapse/handlers/device.py +++ b/synapse/handlers/device.py @@ -129,7 +129,6 @@ def __init__(self, hs: "HomeServer"): 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/rest/client/devices.py b/synapse/rest/client/devices.py index b39ca6a4835..4b84131d326 100644 --- a/synapse/rest/client/devices.py +++ b/synapse/rest/client/devices.py @@ -55,7 +55,6 @@ def __init__(self, hs: "HomeServer"): 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 @@ async def on_GET(self, request: SynapseRequest) -> tuple[int, JsonDict]: 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 @@ def __init__(self, hs: "HomeServer"): 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 @@ async def on_GET( 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 From 3d960d88b38c93fa82be2e98e57c9f146460f056 Mon Sep 17 00:00:00 2001 From: Eric Eastwood Date: Wed, 18 Mar 2026 07:53:13 -0500 Subject: [PATCH 079/121] Add MSC3820 comment context to `RoomVersion` attributes (#19577) Spawning from https://github.com/element-hq/synapse/pull/19424#discussion_r2855303614 --- changelog.d/19577.misc | 1 + synapse/api/room_versions.py | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) create mode 100644 changelog.d/19577.misc diff --git a/changelog.d/19577.misc b/changelog.d/19577.misc new file mode 100644 index 00000000000..d26790e490d --- /dev/null +++ b/changelog.d/19577.misc @@ -0,0 +1 @@ +Add MSC3820 comment context to `RoomVersion` attributes. diff --git a/synapse/api/room_versions.py b/synapse/api/room_versions.py index 2f98d7a8a86..13c7758638d 100644 --- a/synapse/api/room_versions.py +++ b/synapse/api/room_versions.py @@ -86,9 +86,9 @@ class RoomVersion: # MSC2209: Check 'notifications' key while verifying # m.room.power_levels auth rules. limit_notifications_power_levels: bool - # No longer include the creator in m.room.create events. + # MSC3820: No longer include the creator in m.room.create events (room version 11) implicit_room_creator: bool - # Apply updated redaction rules algorithm from room version 11. + # MSC3820: Apply updated redaction rules algorithm from room version 11 updated_redaction_rules: bool # Support the 'restricted' join rule. restricted_join_rule: bool From 8201e58767333aca988b42ef0fee633d329a5b23 Mon Sep 17 00:00:00 2001 From: Tulir Asokan Date: Wed, 18 Mar 2026 16:29:36 +0200 Subject: [PATCH 080/121] Update and stabilize mutual rooms support (MSC2666) (#19511) Updates the error codes to match MSC2666 changes (user ID query param validation + proper errcode for requesting rooms with self), added the new `count` field, and stabilized the endpoint. --- changelog.d/19511.feature | 1 + synapse/config/experimental.py | 3 -- synapse/rest/client/mutual_rooms.py | 35 ++++++++++------ synapse/rest/client/versions.py | 2 +- synapse/types/__init__.py | 58 ++++++++++++++++++++++++-- tests/rest/client/test_mutual_rooms.py | 54 ++++++++++++++---------- 6 files changed, 110 insertions(+), 43 deletions(-) create mode 100644 changelog.d/19511.feature diff --git a/changelog.d/19511.feature b/changelog.d/19511.feature new file mode 100644 index 00000000000..bb9a38b9c13 --- /dev/null +++ b/changelog.d/19511.feature @@ -0,0 +1 @@ +Update and stabilize support for [MSC2666](https://github.com/matrix-org/matrix-spec-proposals/pull/2666): Get rooms in common with another user. Contributed by @tulir @ Beeper. diff --git a/synapse/config/experimental.py b/synapse/config/experimental.py index 41efff1d8f0..0e96b1dc6ac 100644 --- a/synapse/config/experimental.py +++ b/synapse/config/experimental.py @@ -422,9 +422,6 @@ def read_config( # previously calculated push actions. self.msc2654_enabled: bool = experimental.get("msc2654_enabled", False) - # MSC2666: Query mutual rooms between two users. - self.msc2666_enabled: bool = experimental.get("msc2666_enabled", False) - # MSC2815 (allow room moderators to view redacted event content) self.msc2815_enabled: bool = experimental.get("msc2815_enabled", False) diff --git a/synapse/rest/client/mutual_rooms.py b/synapse/rest/client/mutual_rooms.py index a6a913db34d..d6783c15dab 100644 --- a/synapse/rest/client/mutual_rooms.py +++ b/synapse/rest/client/mutual_rooms.py @@ -29,7 +29,7 @@ from synapse.http.server import HttpServer from synapse.http.servlet import RestServlet, parse_strings_from_args from synapse.http.site import SynapseRequest -from synapse.types import JsonDict +from synapse.types import JsonDict, UserID from ._base import client_patterns @@ -65,13 +65,10 @@ def _parse_mutual_rooms_batch_token_args(args: dict[bytes, list[bytes]]) -> str class UserMutualRoomsServlet(RestServlet): """ - GET /uk.half-shot.msc2666/user/mutual_rooms?user_id={user_id}&from={token} HTTP/1.1 + GET /mutual_rooms?user_id={user_id}&from={token} HTTP/1.1 """ - PATTERNS = client_patterns( - "/uk.half-shot.msc2666/user/mutual_rooms$", - releases=(), # This is an unstable feature - ) + PATTERNS = [*client_patterns("/mutual_rooms$", releases=("v1",))] def __init__(self, hs: "HomeServer"): super().__init__() @@ -82,7 +79,9 @@ async def on_GET(self, request: SynapseRequest) -> tuple[int, JsonDict]: # twisted.web.server.Request.args is incorrectly defined as Any | None args: dict[bytes, list[bytes]] = request.args # type: ignore - user_ids = parse_strings_from_args(args, "user_id", required=True) + user_ids = parse_strings_from_args( + args, "user_id", required=True, encoding="utf-8" + ) from_batch = _parse_mutual_rooms_batch_token_args(args) if len(user_ids) > 1: @@ -93,13 +92,19 @@ async def on_GET(self, request: SynapseRequest) -> tuple[int, JsonDict]: ) user_id = user_ids[0] + if not UserID.is_valid_strict(user_id): + raise SynapseError( + HTTPStatus.BAD_REQUEST, + "Invalid user_id query parameter", + errcode=Codes.INVALID_PARAM, + ) requester = await self.auth.get_user_by_req(request) if user_id == requester.user.to_string(): raise SynapseError( HTTPStatus.BAD_REQUEST, "You cannot request a list of shared rooms with yourself", - errcode=Codes.UNKNOWN, + errcode=Codes.INVALID_PARAM, ) # Sort here instead of the database function, so that we don't expose @@ -109,6 +114,7 @@ async def on_GET(self, request: SynapseRequest) -> tuple[int, JsonDict]: frozenset((requester.user.to_string(), user_id)) ) ) + total_count = len(rooms) if from_batch: # A from_batch token was provided, so cut off any rooms where the ID is @@ -123,7 +129,7 @@ async def on_GET(self, request: SynapseRequest) -> tuple[int, JsonDict]: if len(rooms) <= MUTUAL_ROOMS_BATCH_LIMIT: # We've reached the end of the list, don't return a batch token - return 200, {"joined": rooms} + return 200, {"joined": rooms, "count": total_count} rooms = rooms[:MUTUAL_ROOMS_BATCH_LIMIT] # We use urlsafe unpadded base64 encoding for the batch token in order to @@ -135,11 +141,14 @@ async def on_GET(self, request: SynapseRequest) -> tuple[int, JsonDict]: # in the room ID. In the event that some silly user does that, don't let # them paginate further. if next_batch == from_batch: - return 200, {"joined": rooms} + return 200, {"joined": rooms, "count": total_count} - return 200, {"joined": list(rooms), "next_batch": next_batch} + return 200, { + "joined": rooms, + "next_batch": next_batch, + "count": total_count, + } def register_servlets(hs: "HomeServer", http_server: HttpServer) -> None: - if hs.config.experimental.msc2666_enabled: - UserMutualRoomsServlet(hs).register(http_server) + UserMutualRoomsServlet(hs).register(http_server) diff --git a/synapse/rest/client/versions.py b/synapse/rest/client/versions.py index f8d7a1a4d9d..904da867941 100644 --- a/synapse/rest/client/versions.py +++ b/synapse/rest/client/versions.py @@ -144,7 +144,7 @@ async def on_GET(self, request: SynapseRequest) -> tuple[int, JsonDict]: # Implements additional endpoints as described in MSC2432 "org.matrix.msc2432": True, # Implements additional endpoints as described in MSC2666 - "uk.half-shot.msc2666.query_mutual_rooms": self.config.experimental.msc2666_enabled, + "uk.half-shot.msc2666.query_mutual_rooms.stable": True, # Whether new rooms will be set to encrypted or not (based on presets). "io.element.e2ee_forced.public": self.e2ee_forced_public, "io.element.e2ee_forced.private": self.e2ee_forced_private, diff --git a/synapse/types/__init__.py b/synapse/types/__init__.py index fb1f1192b70..02889795bbd 100644 --- a/synapse/types/__init__.py +++ b/synapse/types/__init__.py @@ -343,7 +343,7 @@ def is_valid(cls: type[DS], s: str) -> bool: # possible for invalid data to exist in room-state, etc. parse_and_validate_server_name(obj.domain) return True - except Exception: + except (SynapseError, ValueError): return False __repr__ = to_string @@ -355,6 +355,29 @@ class UserID(DomainSpecificString): SIGIL = "@" + @classmethod + def is_valid_strict(cls, s: str) -> bool: + """ + Parses the input string and attempts to ensure it is a valid and compliant user + ID according to https://spec.matrix.org/v1.17/appendices/#historical-user-ids. + + This should be used with care: there are existing non-compliant user IDs in the + wild with empty or non-ASCII localparts, which will be rejected by this method. + """ + if len(s.encode("utf-8")) > 255: + return False + try: + obj = cls.from_string(s) + if not is_compliant_user_id_localpart(obj.localpart): + return False + # Apply additional validation to the domain. This is only done + # during is_valid (and not part of from_string) since it is + # possible for invalid data to exist in room-state, etc. + parse_and_validate_server_name(obj.domain) + return True + except (SynapseError, ValueError): + return False + @attr.s(slots=True, frozen=True, repr=False) class RoomAlias(DomainSpecificString): @@ -453,12 +476,17 @@ class EventID(DomainSpecificString): def contains_invalid_mxid_characters(localpart: str) -> bool: - """Check for characters not allowed in an mxid or groupid localpart + """ + Check for characters not allowed in a modern user ID localpart. + + This is primarily used for new registrations and MUST NOT be used to validate + existing user IDs, as there are real users whose user IDs don't follow this + character set. + + See https://spec.matrix.org/v1.17/appendices/#user-identifiers Args: localpart: the localpart to be checked - use_extended_character_set: True to use the extended allowed characters - from MSC4009. Returns: True if there are any naughty characters @@ -466,6 +494,28 @@ def contains_invalid_mxid_characters(localpart: str) -> bool: return any(c not in MXID_LOCALPART_ALLOWED_CHARACTERS for c in localpart) +def is_compliant_user_id_localpart(localpart: str) -> bool: + """ + Validates that the given user ID localpart is within the "compliant" range, + i.e. not empty and all characters are between U+0021 and U+007E inclusive. + See https://spec.matrix.org/v1.17/appendices/#historical-user-ids + + To check if a localpart is non-historical, use contains_invalid_mxid_characters instead. + + This should be used with care: there are existing non-compliant user IDs in the + wild with empty or non-ASCII localparts, which will be rejected by this method. + + Args: + localpart: the localpart to be checked + + Returns: + True if the localpart is compliant, False otherwise + """ + if not localpart: + return False + return all(0x21 <= ord(c) <= 0x7E for c in localpart) + + UPPER_CASE_PATTERN = re.compile(b"[A-Z_]") # the following is a pattern which matches '=', and bytes which are not allowed in a mxid diff --git a/tests/rest/client/test_mutual_rooms.py b/tests/rest/client/test_mutual_rooms.py index f78c67fcd97..cbaca17cddb 100644 --- a/tests/rest/client/test_mutual_rooms.py +++ b/tests/rest/client/test_mutual_rooms.py @@ -43,12 +43,6 @@ class UserMutualRoomsTest(unittest.HomeserverTestCase): mutual_rooms.register_servlets, ] - def default_config(self) -> dict: - config = super().default_config() - experimental = config.setdefault("experimental_features", {}) - experimental.setdefault("msc2666_enabled", True) - return config - def make_homeserver(self, reactor: MemoryReactor, clock: Clock) -> HomeServer: config = self.default_config() return self.setup_test_homeserver(config=config) @@ -62,27 +56,12 @@ def _get_mutual_rooms( ) -> FakeChannel: return self.make_request( "GET", - "/_matrix/client/unstable/uk.half-shot.msc2666/user/mutual_rooms" + "/_matrix/client/v1/mutual_rooms" f"?user_id={quote(other_user)}" + (f"&from={quote(since_token)}" if since_token else ""), access_token=token, ) - @unittest.override_config({"experimental_features": {"msc2666_enabled": False}}) - def test_mutual_rooms_no_experimental_flag(self) -> None: - """ - The endpoint should 404 if the experimental flag is not enabled. - """ - # Register a user. - u1 = self.register_user("user1", "pass") - u1_token = self.login(u1, "pass") - - # Check that we're unable to query the endpoint due to the endpoint - # being unrecognised. - channel = self._get_mutual_rooms(u1_token, "@not-used:test") - self.assertEqual(404, channel.code, channel.result) - self.assertEqual("M_UNRECOGNIZED", channel.json_body["errcode"], channel.result) - def test_shared_room_list_public(self) -> None: """ A room should show up in the shared list of rooms between two users @@ -129,6 +108,7 @@ def _check_mutual_rooms_with( channel = self._get_mutual_rooms(u1_token, u2) self.assertEqual(200, channel.code, channel.result) self.assertEqual(len(channel.json_body["joined"]), 1) + self.assertEqual(channel.json_body["count"], 1) self.assertEqual(channel.json_body["joined"][0], room_id_one) # Create another room and invite user2 to it @@ -142,6 +122,7 @@ def _check_mutual_rooms_with( channel = self._get_mutual_rooms(u1_token, u2) self.assertEqual(200, channel.code, channel.result) self.assertEqual(len(channel.json_body["joined"]), 2) + self.assertEqual(channel.json_body["count"], 2) for room_id_id in channel.json_body["joined"]: self.assertIn(room_id_id, [room_id_one, room_id_two]) @@ -167,11 +148,13 @@ def test_shared_room_list_pagination_two_pages(self) -> None: channel = self._get_mutual_rooms(u1_token, u2) self.assertEqual(200, channel.code, channel.result) self.assertEqual(channel.json_body["joined"], room_ids[0:10]) + self.assertEqual(channel.json_body["count"], 15) self.assertIn("next_batch", channel.json_body) channel = self._get_mutual_rooms(u1_token, u2, channel.json_body["next_batch"]) self.assertEqual(200, channel.code, channel.result) self.assertEqual(channel.json_body["joined"], room_ids[10:20]) + self.assertEqual(channel.json_body["count"], 15) self.assertNotIn("next_batch", channel.json_body) def test_shared_room_list_pagination_one_page(self) -> None: @@ -180,6 +163,7 @@ def test_shared_room_list_pagination_one_page(self) -> None: channel = self._get_mutual_rooms(u1_token, u2) self.assertEqual(200, channel.code, channel.result) self.assertEqual(channel.json_body["joined"], room_ids) + self.assertEqual(channel.json_body["count"], 10) self.assertNotIn("next_batch", channel.json_body) def test_shared_room_list_pagination_invalid_token(self) -> None: @@ -209,6 +193,7 @@ def test_shared_room_list_after_leave(self) -> None: channel = self._get_mutual_rooms(u1_token, u2) self.assertEqual(200, channel.code, channel.result) self.assertEqual(len(channel.json_body["joined"]), 1) + self.assertEqual(channel.json_body["count"], 1) self.assertEqual(channel.json_body["joined"][0], room) self.helper.leave(room, user=u1, tok=u1_token) @@ -217,11 +202,13 @@ def test_shared_room_list_after_leave(self) -> None: channel = self._get_mutual_rooms(u1_token, u2) self.assertEqual(200, channel.code, channel.result) self.assertEqual(len(channel.json_body["joined"]), 0) + self.assertEqual(channel.json_body["count"], 0) # Check user2's view of shared rooms with user1 channel = self._get_mutual_rooms(u2_token, u1) self.assertEqual(200, channel.code, channel.result) self.assertEqual(len(channel.json_body["joined"]), 0) + self.assertEqual(channel.json_body["count"], 0) def test_shared_room_list_nonexistent_user(self) -> None: u1 = self.register_user("user1", "pass") @@ -232,4 +219,27 @@ def test_shared_room_list_nonexistent_user(self) -> None: channel = self._get_mutual_rooms(u1_token, "@meow:example.com") self.assertEqual(200, channel.code, channel.result) self.assertEqual(len(channel.json_body["joined"]), 0) + self.assertEqual(channel.json_body["count"], 0) self.assertNotIn("next_batch", channel.json_body) + + def test_shared_room_list_invalid_user(self) -> None: + u1 = self.register_user("user1", "pass") + u1_token = self.login(u1, "pass") + + channel = self._get_mutual_rooms(u1_token, "@:example.com") + self.assertEqual(400, channel.code, channel.result) + self.assertEqual( + "M_INVALID_PARAM", channel.json_body["errcode"], channel.result + ) + + channel = self._get_mutual_rooms(u1_token, "@" + "a" * 255 + ":example.com") + self.assertEqual(400, channel.code, channel.result) + self.assertEqual( + "M_INVALID_PARAM", channel.json_body["errcode"], channel.result + ) + + channel = self._get_mutual_rooms(u1_token, "@🐈️:example.com") + self.assertEqual(400, channel.code, channel.result) + self.assertEqual( + "M_INVALID_PARAM", channel.json_body["errcode"], channel.result + ) From 261bfb786fd70c5d62237db291b090d606e9f723 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Wed, 18 Mar 2026 09:50:09 -0600 Subject: [PATCH 081/121] Fix zeroing out remote quarantined media count (#19559) Just something I noticed while working on https://github.com/element-hq/synapse/pull/19558 We start the function by setting `total_media_quarantined` to zero, then we do work on the `media_ids`, add the number affected, zero it out (**bug**), do work on `hashes`, add the number of affected rows, then return `total_media_quarantined`. ### Pull Request Checklist * [x] Pull request is based on the develop branch * [x] Pull request includes a [changelog file](https://element-hq.github.io/synapse/latest/development/contributing_guide.html#changelog). The entry should: - Be a short description of your change which makes sense to users. "Fixed a bug that prevented receiving messages from other servers." instead of "Moved X method from `EventStore` to `EventWorkerStore`.". - Use markdown where necessary, mostly for `code blocks`. - End with either a period (.) or an exclamation mark (!). - Start with a capital letter. - Feel free to credit yourself, by adding a sentence "Contributed by @github_username." or "Contributed by [Your Name]." to the end of the entry. * [x] [Code style](https://element-hq.github.io/synapse/latest/code_style.html) is correct (run the [linters](https://element-hq.github.io/synapse/latest/development/contributing_guide.html#run-the-linters)) --- changelog.d/19559.bugfix | 1 + synapse/storage/databases/main/room.py | 1 - 2 files changed, 1 insertion(+), 1 deletion(-) create mode 100644 changelog.d/19559.bugfix diff --git a/changelog.d/19559.bugfix b/changelog.d/19559.bugfix new file mode 100644 index 00000000000..f64a84e8e02 --- /dev/null +++ b/changelog.d/19559.bugfix @@ -0,0 +1 @@ +Fix quarantine media admin APIs sometimes returning inaccurate counts for remote media. \ No newline at end of file diff --git a/synapse/storage/databases/main/room.py b/synapse/storage/databases/main/room.py index 633df077367..7ac88e4c2aa 100644 --- a/synapse/storage/databases/main/room.py +++ b/synapse/storage/databases/main/room.py @@ -1217,7 +1217,6 @@ def _quarantine_remote_media_txn( txn.execute(sql, [quarantined_by] + sql_args) total_media_quarantined += txn.rowcount if txn.rowcount > 0 else 0 - total_media_quarantined = 0 if hashes: sql_many_clause_sql, sql_many_clause_args = make_in_list_sql_clause( txn.database_engine, "sha256", hashes From edf5ce277adda9e0028decd0b9e131cd686b18d6 Mon Sep 17 00:00:00 2001 From: Quentin Gliech Date: Wed, 18 Mar 2026 19:47:17 +0100 Subject: [PATCH 082/121] Allow using HTTP/2 over plaintext when introspecting tokens with MAS (#19586) --- changelog.d/19586.feature | 1 + docs/usage/configuration/config_documentation.md | 2 ++ schema/synapse-config.schema.yaml | 7 +++++++ synapse/api/auth/mas.py | 1 + synapse/config/mas.py | 2 ++ 5 files changed, 13 insertions(+) create mode 100644 changelog.d/19586.feature diff --git a/changelog.d/19586.feature b/changelog.d/19586.feature new file mode 100644 index 00000000000..a10bd2d2ef1 --- /dev/null +++ b/changelog.d/19586.feature @@ -0,0 +1 @@ +Introduce a [configuration option](https://element-hq.github.io/synapse/latest/usage/configuration/config_documentation.html#matrix_authentication_service) to allow using HTTP/2 over plaintext when Synapse connects to Matrix Authentication Service. diff --git a/docs/usage/configuration/config_documentation.md b/docs/usage/configuration/config_documentation.md index 1335def5857..48f33d54270 100644 --- a/docs/usage/configuration/config_documentation.md +++ b/docs/usage/configuration/config_documentation.md @@ -653,6 +653,8 @@ This setting has the following sub-options: * `endpoint` (string): The URL where Synapse can reach MAS. This *must* have the `discovery` and `oauth` resources mounted. Defaults to `"http://localhost:8080"`. +* `force_http2` (boolean): Force HTTP/2 over plaintext (H2C) when connecting to MAS. MAS supports this natively, but a reverse proxy between Synapse and MAS may not. Defaults to `false`. + * `secret` (string|null): A shared secret that will be used to authenticate requests from and to MAS. * `secret_path` (string|null): Alternative to `secret`, reading the shared secret from a file. The file should be a plain text file, containing only the secret. Synapse reads the secret from the given file once at startup. diff --git a/schema/synapse-config.schema.yaml b/schema/synapse-config.schema.yaml index dbf7d7acb7d..aaa306cee05 100644 --- a/schema/synapse-config.schema.yaml +++ b/schema/synapse-config.schema.yaml @@ -677,6 +677,13 @@ properties: and `oauth` resources mounted. default: http://localhost:8080 + force_http2: + type: boolean + description: >- + Force HTTP/2 over plaintext (H2C) when connecting to MAS. MAS supports + this natively, but a reverse proxy between Synapse and MAS may not. + default: false + secret: type: ["string", "null"] description: >- diff --git a/synapse/api/auth/mas.py b/synapse/api/auth/mas.py index 79c15a53296..95c6d62a9d7 100644 --- a/synapse/api/auth/mas.py +++ b/synapse/api/auth/mas.py @@ -111,6 +111,7 @@ def __init__(self, hs: "HomeServer"): self._rust_http_client = HttpClient( reactor=hs.get_reactor(), user_agent=self._http_client.user_agent.decode("utf8"), + http2_only=self._config.force_http2, ) self._server_metadata = RetryOnExceptionCachedCall[ServerMetadata]( self._load_metadata diff --git a/synapse/config/mas.py b/synapse/config/mas.py index 104ba17ab79..6973e9ae58b 100644 --- a/synapse/config/mas.py +++ b/synapse/config/mas.py @@ -36,6 +36,7 @@ class MasConfigModel(ParseModel): enabled: StrictBool = False endpoint: AnyHttpUrl = AnyHttpUrl("http://localhost:8080") + force_http2: StrictBool = False secret: StrictStr | None = Field(default=None) # We set `strict=False` to allow `str` instances. secret_path: FilePath | None = Field(default=None, strict=False) @@ -82,6 +83,7 @@ def read_config( self.enabled = parsed.enabled self.endpoint = parsed.endpoint + self.force_http2 = parsed.force_http2 self._secret = parsed.secret self._secret_path = parsed.secret_path From 90697637a8f37faf898350318a231813a942bd74 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 18 Mar 2026 19:16:14 +0000 Subject: [PATCH 083/121] Bump actions/setup-go from 6.2.0 to 6.3.0 in the minor-and-patches group (#19564) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/latest_deps.yml | 2 +- .github/workflows/tests.yml | 2 +- .github/workflows/twisted_trunk.yml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/latest_deps.yml b/.github/workflows/latest_deps.yml index 2d945c2096b..450be20956f 100644 --- a/.github/workflows/latest_deps.yml +++ b/.github/workflows/latest_deps.yml @@ -207,7 +207,7 @@ jobs: - name: Prepare Complement's Prerequisites run: synapse/.ci/scripts/setup_complement_prerequisites.sh - - uses: actions/setup-go@7a3fe6cf4cb3a834922a1244abfce67bcef6a0c5 # v6.2.0 + - uses: actions/setup-go@4b73464bb391d4059bd26b0524d20df3927bd417 # v6.3.0 with: cache-dependency-path: complement/go.sum go-version-file: complement/go.mod diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index a03d27472d7..987046b741f 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -715,7 +715,7 @@ jobs: - name: Prepare Complement's Prerequisites run: synapse/.ci/scripts/setup_complement_prerequisites.sh - - uses: actions/setup-go@7a3fe6cf4cb3a834922a1244abfce67bcef6a0c5 # v6.2.0 + - uses: actions/setup-go@4b73464bb391d4059bd26b0524d20df3927bd417 # v6.3.0 with: cache-dependency-path: complement/go.sum go-version-file: complement/go.mod diff --git a/.github/workflows/twisted_trunk.yml b/.github/workflows/twisted_trunk.yml index d38e38ebcb5..74b4f11722c 100644 --- a/.github/workflows/twisted_trunk.yml +++ b/.github/workflows/twisted_trunk.yml @@ -180,7 +180,7 @@ jobs: - name: Prepare Complement's Prerequisites run: synapse/.ci/scripts/setup_complement_prerequisites.sh - - uses: actions/setup-go@7a3fe6cf4cb3a834922a1244abfce67bcef6a0c5 # v6.2.0 + - uses: actions/setup-go@4b73464bb391d4059bd26b0524d20df3927bd417 # v6.3.0 with: cache-dependency-path: complement/go.sum go-version-file: complement/go.mod From 9fe7cbfe7f69a69fb0f445f16a9753a611c24e58 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 18 Mar 2026 19:24:28 +0000 Subject: [PATCH 084/121] Bump actions/upload-artifact from 6.0.0 to 7.0.0 (#19565) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/docker.yml | 2 +- .github/workflows/docs-pr.yaml | 2 +- .github/workflows/latest_deps.yml | 2 +- .github/workflows/release-artifacts.yml | 6 +++--- .github/workflows/tests.yml | 4 ++-- .github/workflows/twisted_trunk.yml | 2 +- 6 files changed, 9 insertions(+), 9 deletions(-) diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml index b1dd8a75e77..a796f9cf2a8 100644 --- a/.github/workflows/docker.yml +++ b/.github/workflows/docker.yml @@ -108,7 +108,7 @@ jobs: touch "${{ runner.temp }}/digests/${digest#sha256:}" - name: Upload digest - uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0 + uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 with: name: digests-${{ matrix.suffix }} path: ${{ runner.temp }}/digests/* diff --git a/.github/workflows/docs-pr.yaml b/.github/workflows/docs-pr.yaml index 41f3664336f..cba12937436 100644 --- a/.github/workflows/docs-pr.yaml +++ b/.github/workflows/docs-pr.yaml @@ -39,7 +39,7 @@ jobs: cp book/welcome_and_overview.html book/index.html - name: Upload Artifact - uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0 + uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 with: name: book path: book diff --git a/.github/workflows/latest_deps.yml b/.github/workflows/latest_deps.yml index 450be20956f..a85bad9d743 100644 --- a/.github/workflows/latest_deps.yml +++ b/.github/workflows/latest_deps.yml @@ -172,7 +172,7 @@ jobs: if: ${{ always() }} run: /sytest/scripts/tap_to_gha.pl /logs/results.tap - name: Upload SyTest logs - uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0 + uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 if: ${{ always() }} with: name: Sytest Logs - ${{ job.status }} - (${{ join(matrix.*, ', ') }}) diff --git a/.github/workflows/release-artifacts.yml b/.github/workflows/release-artifacts.yml index 4343cb482f2..125d659f4e1 100644 --- a/.github/workflows/release-artifacts.yml +++ b/.github/workflows/release-artifacts.yml @@ -99,7 +99,7 @@ jobs: echo "ARTIFACT_NAME=${DISTRO#*:}" >> "$GITHUB_OUTPUT" - name: Upload debs as artifacts - uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0 + uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 with: name: debs-${{ steps.artifact-name.outputs.ARTIFACT_NAME }} path: debs/* @@ -150,7 +150,7 @@ jobs: # musl: (TODO: investigate). CIBW_TEST_SKIP: pp3*-* *musl* - - uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0 + - uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 with: name: Wheel-${{ matrix.os }} path: ./wheelhouse/*.whl @@ -171,7 +171,7 @@ jobs: - name: Build sdist run: python -m build --sdist - - uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0 + - uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 with: name: Sdist path: dist/*.tar.gz diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 987046b741f..35c633b8edf 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -561,7 +561,7 @@ jobs: if: ${{ always() }} run: /sytest/scripts/tap_to_gha.pl /logs/results.tap - name: Upload SyTest logs - uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0 + uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 if: ${{ always() }} with: name: Sytest Logs - ${{ job.status }} - (${{ join(matrix.job.*, ', ') }}) @@ -658,7 +658,7 @@ jobs: PGPASSWORD: postgres PGDATABASE: postgres - name: "Upload schema differences" - uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0 + uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 if: ${{ failure() && !cancelled() && steps.run_tester_script.outcome == 'failure' }} with: name: Schema dumps diff --git a/.github/workflows/twisted_trunk.yml b/.github/workflows/twisted_trunk.yml index 74b4f11722c..e39d64cfb2f 100644 --- a/.github/workflows/twisted_trunk.yml +++ b/.github/workflows/twisted_trunk.yml @@ -145,7 +145,7 @@ jobs: if: ${{ always() }} run: /sytest/scripts/tap_to_gha.pl /logs/results.tap - name: Upload SyTest logs - uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0 + uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 if: ${{ always() }} with: name: Sytest Logs - ${{ job.status }} - (${{ join(matrix.*, ', ') }}) From f490c49c8580c6b404dd3dc311da30cc3cca9529 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 19 Mar 2026 10:54:04 +0000 Subject: [PATCH 085/121] Bump pyasn1 from 0.6.2 to 0.6.3 (#19584) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- poetry.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/poetry.lock b/poetry.lock index 681a183b422..36d3035277e 100644 --- a/poetry.lock +++ b/poetry.lock @@ -2076,14 +2076,14 @@ psycopg2 = "*" [[package]] name = "pyasn1" -version = "0.6.2" +version = "0.6.3" description = "Pure-Python implementation of ASN.1 types and DER/BER/CER codecs (X.208)" optional = false python-versions = ">=3.8" groups = ["main"] files = [ - {file = "pyasn1-0.6.2-py3-none-any.whl", hash = "sha256:1eb26d860996a18e9b6ed05e7aae0e9fc21619fcee6af91cca9bad4fbea224bf"}, - {file = "pyasn1-0.6.2.tar.gz", hash = "sha256:9b59a2b25ba7e4f8197db7686c09fb33e658b98339fadb826e9512629017833b"}, + {file = "pyasn1-0.6.3-py3-none-any.whl", hash = "sha256:a80184d120f0864a52a073acc6fc642847d0be408e7c7252f31390c0f4eadcde"}, + {file = "pyasn1-0.6.3.tar.gz", hash = "sha256:697a8ecd6d98891189184ca1fa05d1bb00e2f84b5977c481452050549c8a72cf"}, ] [[package]] From 9edbf56969af7a4aa894237fa58bb1042947754b Mon Sep 17 00:00:00 2001 From: Andrew Morgan <1342360+anoadragon453@users.noreply.github.com> Date: Thu, 19 Mar 2026 12:52:40 +0000 Subject: [PATCH 086/121] Prevent sending registration emails if registration is disabled (#19585) --- changelog.d/19585.misc | 1 + synapse/rest/client/register.py | 9 +++++++ tests/rest/client/test_register.py | 40 +++++++++++++++++++++++++++++- 3 files changed, 49 insertions(+), 1 deletion(-) create mode 100644 changelog.d/19585.misc diff --git a/changelog.d/19585.misc b/changelog.d/19585.misc new file mode 100644 index 00000000000..621cf782f14 --- /dev/null +++ b/changelog.d/19585.misc @@ -0,0 +1 @@ +Prevent sending registration emails if registration is disabled. \ No newline at end of file diff --git a/synapse/rest/client/register.py b/synapse/rest/client/register.py index fdd2f1985a3..73832ba7a81 100644 --- a/synapse/rest/client/register.py +++ b/synapse/rest/client/register.py @@ -86,6 +86,7 @@ def __init__(self, hs: "HomeServer"): self.server_name = hs.hostname self.identity_handler = hs.get_identity_handler() self.config = hs.config + self._registration_enabled = hs.config.registration.enable_registration if self.hs.config.email.can_verify_email: self.registration_mailer = Mailer( @@ -109,6 +110,14 @@ async def on_POST(self, request: SynapseRequest) -> tuple[int, JsonDict]: raise SynapseError( 400, "Email-based registration has been disabled on this server" ) + + if not self._registration_enabled: + raise SynapseError( + 403, + "Registration is disabled on this homeserver", + Codes.FORBIDDEN, + ) + body = parse_json_object_from_request(request) assert_params_in_dict(body, ["client_secret", "email", "send_attempt"]) diff --git a/tests/rest/client/test_register.py b/tests/rest/client/test_register.py index 2c0396a3de4..f66c56a6b19 100644 --- a/tests/rest/client/test_register.py +++ b/tests/rest/client/test_register.py @@ -22,7 +22,7 @@ import datetime import importlib.resources as importlib_resources import os -from typing import Any +from typing import Any, cast from unittest.mock import AsyncMock from twisted.internet.testing import MemoryReactor @@ -66,6 +66,17 @@ def make_homeserver( hs.get_send_email_handler()._sendmail = AsyncMock() return hs + def _get_sendmail_mock(self) -> AsyncMock: + """ + Cast the homeserver's `_sendmail` object as an `AsyncMock`. + + `_sendmail` is an `AsyncMock` (see `make_homeserver`) but this type + information doesn't make it through the test harness. Thus we need to + cast the object again. + """ + sendmail = self.hs.get_send_email_handler()._sendmail + return cast(AsyncMock, sendmail) + def test_POST_appservice_registration_valid(self) -> None: user_id = "@as_user_kermit:test" as_token = "i_am_an_app_service" @@ -747,6 +758,33 @@ def test_request_token_existing_email_inhibit_error(self) -> None: self.assertIsNotNone(channel.json_body.get("sid")) + @unittest.override_config( + { + "public_baseurl": "https://test_server", + "email": { + "smtp_host": "mail_server", + "smtp_port": 2525, + "notif_from": "sender@host", + }, + } + ) + def test_request_token_allowed_when_email_flow_is_advertised(self) -> None: + sendmail = self._get_sendmail_mock() + sendmail.reset_mock() + + channel = self.make_request( + "POST", + b"register/email/requestToken", + { + "client_secret": "foobar", + "email": "test@example.com", + "send_attempt": 1, + }, + ) + self.assertEqual(200, channel.code, channel.result) + self.assertIsNotNone(channel.json_body.get("sid")) + sendmail.assert_awaited_once() + @unittest.override_config( { "public_baseurl": "https://test_server", From 2c412ba24ae013c657128f6ff2256a4e23b181ae Mon Sep 17 00:00:00 2001 From: Andrew Morgan <1342360+anoadragon453@users.noreply.github.com> Date: Fri, 20 Mar 2026 16:30:03 +0000 Subject: [PATCH 087/121] complement.sh: ensure old complement checkout files are deleted; remove -N `wget` flag (#19592) --- changelog.d/19592.misc | 1 + scripts-dev/complement.sh | 12 +++++++++++- 2 files changed, 12 insertions(+), 1 deletion(-) create mode 100644 changelog.d/19592.misc diff --git a/changelog.d/19592.misc b/changelog.d/19592.misc new file mode 100644 index 00000000000..e3fa3a5416b --- /dev/null +++ b/changelog.d/19592.misc @@ -0,0 +1 @@ +Ensure old Complement test files are removed when downloading a Complement checkout via `./scripts-dev/complement.sh`. \ No newline at end of file diff --git a/scripts-dev/complement.sh b/scripts-dev/complement.sh index a8a361fd4a6..cca87d42a9e 100755 --- a/scripts-dev/complement.sh +++ b/scripts-dev/complement.sh @@ -160,8 +160,18 @@ main() { if [[ -z "$COMPLEMENT_DIR" ]]; then COMPLEMENT_REF=${COMPLEMENT_REF:-main} echo "COMPLEMENT_DIR not set. Fetching Complement checkout from ${COMPLEMENT_REF}..." - wget -Nq https://github.com/matrix-org/complement/archive/${COMPLEMENT_REF}.tar.gz + + # Download the Complement checkout at the specified ref. + wget -q https://github.com/matrix-org/complement/archive/${COMPLEMENT_REF}.tar.gz + + # Delete the existing complement checkout. Otherwise we'll end up with stale + # test files after they're deleted server-side, and `tar` will not delete + # old files. + rm -rf complement-${COMPLEMENT_REF} + + # Extract the checkout. tar -xzf ${COMPLEMENT_REF}.tar.gz + COMPLEMENT_DIR=complement-${COMPLEMENT_REF} echo "Checkout available at 'complement-${COMPLEMENT_REF}'" fi From b4282b82d0dbc3ba34954d442a581ad9331e465a Mon Sep 17 00:00:00 2001 From: Hugh Nimmo-Smith Date: Fri, 20 Mar 2026 16:33:43 +0000 Subject: [PATCH 088/121] Updates for experimental MSC4388 support (sign-in with QR code) (#19573) --- changelog.d/19573.feature | 1 + synapse/config/experimental.py | 4 +- synapse/rest/client/rendezvous.py | 7 ++++ synapse/rest/client/versions.py | 2 - tests/rest/client/test_msc4388_rendezvous.py | 42 ++++++++++++++------ 5 files changed, 40 insertions(+), 16 deletions(-) create mode 100644 changelog.d/19573.feature diff --git a/changelog.d/19573.feature b/changelog.d/19573.feature new file mode 100644 index 00000000000..69eca83681f --- /dev/null +++ b/changelog.d/19573.feature @@ -0,0 +1 @@ +Updated experimental support for [MSC4388: Secure out-of-band channel for sign in with QR](https://github.com/matrix-org/matrix-spec-proposals/pull/4388). diff --git a/synapse/config/experimental.py b/synapse/config/experimental.py index 0e96b1dc6ac..ff9b394eb9e 100644 --- a/synapse/config/experimental.py +++ b/synapse/config/experimental.py @@ -538,9 +538,9 @@ def read_config( # See: https://github.com/element-hq/synapse/issues/19433 msc4388_mode = experimental.get("msc4388_mode", "off") - if msc4388_mode not in ["off", "public", "authenticated"]: + if msc4388_mode not in ["off", "open", "authenticated"]: raise ConfigError( - "msc4388_mode must be one of 'off', 'public' or 'authenticated'", + "msc4388_mode must be one of 'off', 'open' or 'authenticated'", ("experimental", "msc4388_mode"), ) self.msc4388_enabled: bool = msc4388_mode != "off" diff --git a/synapse/rest/client/rendezvous.py b/synapse/rest/client/rendezvous.py index bd9205fc5f0..246788609e2 100644 --- a/synapse/rest/client/rendezvous.py +++ b/synapse/rest/client/rendezvous.py @@ -20,6 +20,7 @@ # import logging +from http import HTTPStatus from http.client import TEMPORARY_REDIRECT from typing import TYPE_CHECKING, Any @@ -81,6 +82,12 @@ def __init__(self, hs: "HomeServer") -> None: hs.config.experimental.msc4388_requires_authentication ) + async def on_GET(self, request: SynapseRequest) -> tuple[int, Any]: + if self.require_authentication: + # This will raise if the user is not authenticated + await self.auth.get_user_by_req(request) + return HTTPStatus.OK, {"create_available": True} + async def on_POST(self, request: SynapseRequest) -> tuple[int, Any]: if self.require_authentication: # This will raise if the user is not authenticated diff --git a/synapse/rest/client/versions.py b/synapse/rest/client/versions.py index 904da867941..7bf4b12e8b7 100644 --- a/synapse/rest/client/versions.py +++ b/synapse/rest/client/versions.py @@ -189,8 +189,6 @@ async def on_GET(self, request: SynapseRequest) -> tuple[int, JsonDict]: is not None ) ), - # MSC4388: Secure out-of-band channel for sign in with QR - "io.element.msc4388": (self.config.experimental.msc4388_enabled), # MSC4140: Delayed events "org.matrix.msc4140": bool(self.config.server.max_event_delay_ms), # Simplified sliding sync diff --git a/tests/rest/client/test_msc4388_rendezvous.py b/tests/rest/client/test_msc4388_rendezvous.py index f16bb3f344a..913a41dedb2 100644 --- a/tests/rest/client/test_msc4388_rendezvous.py +++ b/tests/rest/client/test_msc4388_rendezvous.py @@ -131,6 +131,10 @@ def test_disabled(self) -> None: } ) def test_off(self) -> None: + # Discovery endpoint should return 404 + channel = self.make_request("GET", rz_endpoint, {}, access_token=None) + self.assertEqual(channel.code, 404) + # Create session should also fail channel = self.make_request("POST", rz_endpoint, {}, access_token=None) self.assertEqual(channel.code, 404) @@ -143,7 +147,7 @@ def test_off(self) -> None: "endpoint": "https://issuer", }, "experimental_features": { - "msc4388_mode": "public", + "msc4388_mode": "open", }, } ) @@ -156,6 +160,11 @@ def test_rendezvous_public(self) -> None: - Deleting the data - Sequence token handling """ + # Discovery should return 200 + channel = self.make_request("GET", rz_endpoint, {}, access_token=None) + self.assertEqual(channel.code, 200) + self.assertTrue(channel.json_body.get("create_available")) + # We can post arbitrary data to the endpoint channel = self.make_request( "POST", @@ -268,7 +277,11 @@ def test_rendezvous_requires_authentication(self) -> None: self.setup_mock_oauth() alice_token = self.register_oauth_user("alice", "device1") - # This should fail without authentication: + # Discovery should fail due to lack of authentication + channel = self.make_request("GET", rz_endpoint, {}, access_token=None) + self.assertEqual(channel.code, 401) + + # Creating a session should fail without authentication: channel = self.make_request( "POST", rz_endpoint, @@ -277,6 +290,11 @@ def test_rendezvous_requires_authentication(self) -> None: ) self.assertEqual(channel.code, 401) + # Discovery should succeed with authentication + channel = self.make_request("GET", rz_endpoint, {}, access_token=alice_token) + self.assertEqual(channel.code, 200) + self.assertTrue(channel.json_body.get("create_available")) + # This should work as we are now authenticated channel = self.make_request( "POST", @@ -288,7 +306,7 @@ def test_rendezvous_requires_authentication(self) -> None: rendezvous_id = channel.json_body["id"] sequence_token = channel.json_body["sequence_token"] expires_in_ms = channel.json_body["expires_in_ms"] - self.assertGreater(expires_in_ms, 0) + self.assertEqual(expires_in_ms, 120000) session_endpoint = rz_endpoint + f"/{rendezvous_id}" @@ -302,7 +320,7 @@ def test_rendezvous_requires_authentication(self) -> None: self.assertEqual(channel.code, 200) self.assertEqual(channel.json_body["data"], "foo=bar") self.assertEqual(channel.json_body["sequence_token"], sequence_token) - self.assertEqual(channel.json_body["expires_in_ms"], expires_in_ms) + self.assertEqual(channel.json_body["expires_in_ms"], expires_in_ms - 100) # We can update the data without authentication channel = self.make_request( @@ -325,7 +343,7 @@ def test_rendezvous_requires_authentication(self) -> None: self.assertEqual(channel.code, 200) self.assertEqual(channel.json_body["data"], "foo=baz") self.assertEqual(channel.json_body["sequence_token"], new_sequence_token) - self.assertEqual(channel.json_body["expires_in_ms"], expires_in_ms - 200) + self.assertEqual(channel.json_body["expires_in_ms"], expires_in_ms - 300) # We can delete the data without authentication channel = self.make_request( @@ -355,7 +373,7 @@ def test_rendezvous_requires_authentication(self) -> None: "endpoint": "https://issuer", }, "experimental_features": { - "msc4388_mode": "public", + "msc4388_mode": "open", }, } ) @@ -402,7 +420,7 @@ def test_expiration(self) -> None: "endpoint": "https://issuer", }, "experimental_features": { - "msc4388_mode": "public", + "msc4388_mode": "open", }, } ) @@ -473,7 +491,7 @@ def test_capacity(self) -> None: "endpoint": "https://issuer", }, "experimental_features": { - "msc4388_mode": "public", + "msc4388_mode": "open", }, } ) @@ -531,7 +549,7 @@ def test_hard_capacity(self) -> None: "endpoint": "https://issuer", }, "experimental_features": { - "msc4388_mode": "public", + "msc4388_mode": "open", }, } ) @@ -585,7 +603,7 @@ def test_data_type(self) -> None: "endpoint": "https://issuer", }, "experimental_features": { - "msc4388_mode": "public", + "msc4388_mode": "open", }, } ) @@ -648,7 +666,7 @@ def test_max_length(self) -> None: "endpoint": "https://issuer", }, "experimental_features": { - "msc4388_mode": "public", + "msc4388_mode": "open", }, } ) @@ -698,7 +716,7 @@ def test_rendezvous_rejects_unsafe_get_requests( "endpoint": "https://issuer", }, "experimental_features": { - "msc4388_mode": "public", + "msc4388_mode": "open", }, } ) From 40d699b1d4d7855ffb892723eac90cd34f22aa6f Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Fri, 20 Mar 2026 13:34:26 -0600 Subject: [PATCH 089/121] Stable support for MSC4284 policy servers (#19503) Fixes https://github.com/element-hq/synapse/issues/19494 MSC4284 policy servers This: * removes the old `/check` (recommendation) support because it's from an older design. Policy servers should have updated to `/sign` by now. We also remove optionality around the policy server's public key because it was only optional to support `/check`. * supports the stable `m.room.policy` state event and `/sign` endpoints, falling back to unstable if required. Note the changes between unstable and stable: * Stable `/sign` uses errors instead of an empty signatures block to indicate refusal. * Stable `m.room.policy` nests the public key in an object with explicit key algorithm (always ed25519 for now) * does *not* introduce tests that the above fallback to unstable works. If it breaks, we're not going to be sad about an early transition. Tests can be added upon request, though. * fixes a bug where the policy server was asked to sign policy server state events (the events were correctly skipped in `is_event_allowed`, but `ask_policy_server_to_sign_event` didn't do the same). * fixes a bug where the original event sender's signature can be deleted if the sending server is the same as the policy server. * proxies Matrix-shaped errors from the policy server to the Client-Server API as `SynapseError`s (a new capability of the stable API). Membership event handling (from the issue) is expected to be a different PR due to the size of changes involved (tracked by https://github.com/element-hq/synapse/issues/19587). ### Pull Request Checklist * [x] Pull request is based on the develop branch * [x] Pull request includes a [changelog file](https://element-hq.github.io/synapse/latest/development/contributing_guide.html#changelog). The entry should: - Be a short description of your change which makes sense to users. "Fixed a bug that prevented receiving messages from other servers." instead of "Moved X method from `EventStore` to `EventWorkerStore`.". - Use markdown where necessary, mostly for `code blocks`. - End with either a period (.) or an exclamation mark (!). - Start with a capital letter. - Feel free to credit yourself, by adding a sentence "Contributed by @github_username." or "Contributed by [Your Name]." to the end of the entry. * [x] [Code style](https://element-hq.github.io/synapse/latest/code_style.html) is correct (run the [linters](https://element-hq.github.io/synapse/latest/development/contributing_guide.html#run-the-linters)) --------- Co-authored-by: turt2live <1190097+turt2live@users.noreply.github.com> Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> Co-authored-by: Eric Eastwood --- changelog.d/19503.bugfix.1 | 1 + changelog.d/19503.bugfix.2 | 1 + changelog.d/19503.feature | 1 + synapse/api/constants.py | 2 + synapse/federation/federation_client.py | 75 +------ synapse/federation/transport/client.py | 57 +++--- synapse/handlers/room_policy.py | 249 ++++++++++++++++-------- synapse/types/handlers/policy_server.py | 16 -- tests/handlers/test_room_policy.py | 96 ++++----- 9 files changed, 249 insertions(+), 249 deletions(-) create mode 100644 changelog.d/19503.bugfix.1 create mode 100644 changelog.d/19503.bugfix.2 create mode 100644 changelog.d/19503.feature delete mode 100644 synapse/types/handlers/policy_server.py diff --git a/changelog.d/19503.bugfix.1 b/changelog.d/19503.bugfix.1 new file mode 100644 index 00000000000..45e3ad603b2 --- /dev/null +++ b/changelog.d/19503.bugfix.1 @@ -0,0 +1 @@ +Fix [MSC4284](https://github.com/matrix-org/matrix-spec-proposals/pull/4284) Policy Servers implementation to skip signing `org.matrix.msc4284.policy` and `m.room.policy` state events. \ No newline at end of file diff --git a/changelog.d/19503.bugfix.2 b/changelog.d/19503.bugfix.2 new file mode 100644 index 00000000000..8a423ba785d --- /dev/null +++ b/changelog.d/19503.bugfix.2 @@ -0,0 +1 @@ +Correctly apply [MSC4284](https://github.com/matrix-org/matrix-spec-proposals/pull/4284) Policy Server signatures to events when the sender and policy server have the same server name. \ No newline at end of file diff --git a/changelog.d/19503.feature b/changelog.d/19503.feature new file mode 100644 index 00000000000..20db8aeaae1 --- /dev/null +++ b/changelog.d/19503.feature @@ -0,0 +1 @@ +Add stable support for [MSC4284](https://github.com/matrix-org/matrix-spec-proposals/pull/4284) Policy Servers. diff --git a/synapse/api/constants.py b/synapse/api/constants.py index 21139a3867a..eb9e6cc39b9 100644 --- a/synapse/api/constants.py +++ b/synapse/api/constants.py @@ -158,6 +158,8 @@ class EventTypes: PollStart: Final = "m.poll.start" + RoomPolicy: Final = "m.room.policy" + class ToDeviceEventTypes: RoomKeyRequest: Final = "m.room_key_request" diff --git a/synapse/federation/federation_client.py b/synapse/federation/federation_client.py index ba738ad65e7..55151ca549c 100644 --- a/synapse/federation/federation_client.py +++ b/synapse/federation/federation_client.py @@ -72,7 +72,6 @@ from synapse.logging.opentracing import SynapseTags, log_kv, set_tag, tag_args, trace from synapse.metrics import SERVER_NAME_LABEL from synapse.types import JsonDict, StrCollection, UserID, get_domain_from_id -from synapse.types.handlers.policy_server import RECOMMENDATION_OK, RECOMMENDATION_SPAM from synapse.util.async_helpers import concurrently_execute from synapse.util.caches.expiringcache import ExpiringCache from synapse.util.duration import Duration @@ -438,72 +437,16 @@ async def _record_failure_callback( return None - @trace - @tag_args - async def get_pdu_policy_recommendation( - self, destination: str, pdu: EventBase, timeout: int | None = None - ) -> str: - """Requests that the destination server (typically a policy server) - check the event and return its recommendation on how to handle the - event. - - If the policy server could not be contacted or the policy server - returned an unknown recommendation, this returns an OK recommendation. - This type fixing behaviour is done because the typical caller will be - in a critical call path and would generally interpret a `None` or similar - response as "weird value; don't care; move on without taking action". We - just frontload that logic here. - - - Args: - destination: The remote homeserver to ask (a policy server) - pdu: The event to check - timeout: How long to try (in ms) the destination for before - giving up. None indicates no timeout. - - Returns: - The policy recommendation, or RECOMMENDATION_OK if the policy server was - uncontactable or returned an unknown recommendation. - """ - - logger.debug( - "get_pdu_policy_recommendation for event_id=%s from %s", - pdu.event_id, - destination, - ) - - try: - res = await self.transport_layer.get_policy_recommendation_for_pdu( - destination, pdu, timeout=timeout - ) - recommendation = res.get("recommendation") - if not isinstance(recommendation, str): - raise InvalidResponseError("recommendation is not a string") - if recommendation not in (RECOMMENDATION_OK, RECOMMENDATION_SPAM): - logger.warning( - "get_pdu_policy_recommendation: unknown recommendation: %s", - recommendation, - ) - return RECOMMENDATION_OK - return recommendation - except Exception as e: - logger.warning( - "get_pdu_policy_recommendation: server %s responded with error, assuming OK recommendation: %s", - destination, - e, - ) - return RECOMMENDATION_OK - @trace @tag_args async def ask_policy_server_to_sign_event( self, destination: str, pdu: EventBase, timeout: int | None = None - ) -> JsonDict | None: + ) -> JsonDict: """Requests that the destination server (typically a policy server) sign the event as not spam. If the policy server could not be contacted or the policy server - returned an error, this returns no signature. + returned an error, that error is raised. Args: destination: The remote homeserver to ask (a policy server) @@ -519,17 +462,9 @@ async def ask_policy_server_to_sign_event( pdu.event_id, destination, ) - try: - return await self.transport_layer.ask_policy_server_to_sign_event( - destination, pdu, timeout=timeout - ) - except Exception as e: - logger.warning( - "ask_policy_server_to_sign_event: server %s responded with error: %s", - destination, - e, - ) - return None + return await self.transport_layer.ask_policy_server_to_sign_event( + destination, pdu, timeout=timeout + ) @trace @tag_args diff --git a/synapse/federation/transport/client.py b/synapse/federation/transport/client.py index 35d3c30c699..5d5212ef96c 100644 --- a/synapse/federation/transport/client.py +++ b/synapse/federation/transport/client.py @@ -47,6 +47,7 @@ ) from synapse.events import EventBase, make_event_from_dict from synapse.federation.units import Transaction +from synapse.http.client import is_unknown_endpoint from synapse.http.matrixfederationclient import ByteParser, LegacyJsonSendParser from synapse.http.types import QueryParams from synapse.types import JsonDict, UserID @@ -141,33 +142,6 @@ async def get_event( destination, path=path, timeout=timeout, try_trailing_slash_on_400=True ) - async def get_policy_recommendation_for_pdu( - self, destination: str, event: EventBase, timeout: int | None = None - ) -> JsonDict: - """Requests the policy recommendation for the given pdu from the given policy server. - - Args: - destination: The host name of the remote homeserver checking the event. - event: The event to check. - timeout: How long to try (in ms) the destination for before giving up. - None indicates no timeout. - - Returns: - The full recommendation object from the remote server. - """ - logger.debug( - "get_policy_recommendation_for_pdu dest=%s, event_id=%s", - destination, - event.event_id, - ) - return await self.client.post_json( - destination=destination, - path=f"/_matrix/policy/unstable/org.matrix.msc4284/event/{event.event_id}/check", - data=event.get_pdu_json(), - ignore_backoff=True, - timeout=timeout, - ) - async def ask_policy_server_to_sign_event( self, destination: str, event: EventBase, timeout: int | None = None ) -> JsonDict: @@ -186,13 +160,28 @@ async def ask_policy_server_to_sign_event( The signature from the policy server, structured in the same was as the 'signatures' JSON in the event e.g { "$policy_server_via_domain" : { "ed25519:policy_server": "signature_base64" }} """ - return await self.client.post_json( - destination=destination, - path="/_matrix/policy/unstable/org.matrix.msc4284/sign", - data=event.get_pdu_json(), - ignore_backoff=True, - timeout=timeout, - ) + # Try stable first, then fall back to unstable if unsupported. All other errors + # are just errors. + try: + return await self.client.post_json( + destination=destination, + path="/_matrix/policy/v1/sign", + data=event.get_pdu_json(), + ignore_backoff=True, + timeout=timeout, + ) + except HttpResponseException as ex: + if is_unknown_endpoint(ex): + # TODO: Remove unstable MSC4284 support + # https://github.com/element-hq/synapse/issues/19502 + return await self.client.post_json( + destination=destination, + path="/_matrix/policy/unstable/org.matrix.msc4284/sign", + data=event.get_pdu_json(), + ignore_backoff=True, + timeout=timeout, + ) + raise async def backfill( self, destination: str, room_id: str, event_tuples: Collection[str], limit: int diff --git a/synapse/handlers/room_policy.py b/synapse/handlers/room_policy.py index 0663a367144..57163c43444 100644 --- a/synapse/handlers/room_policy.py +++ b/synapse/handlers/room_policy.py @@ -17,13 +17,14 @@ import logging from typing import TYPE_CHECKING +import attr from signedjson.key import decode_verify_key_bytes from unpaddedbase64 import decode_base64 -from synapse.api.errors import SynapseError +from synapse.api.constants import EventTypes +from synapse.api.errors import Codes, HttpResponseException, SynapseError from synapse.crypto.keyring import VerifyJsonRequest from synapse.events import EventBase -from synapse.types.handlers.policy_server import RECOMMENDATION_OK from synapse.util.stringutils import parse_and_validate_server_name if TYPE_CHECKING: @@ -31,10 +32,18 @@ logger = logging.getLogger(__name__) -POLICY_SERVER_EVENT_TYPE = "org.matrix.msc4284.policy" POLICY_SERVER_KEY_ID = "ed25519:policy_server" +@attr.s(slots=True, auto_attribs=True) +class PolicyServerInfo: + # name of the server. + server_name: str + + # the unpadded base64-encoded Ed25519 public key of the server. + public_key: str + + class RoomPolicyHandler: def __init__(self, hs: "HomeServer"): self._hs = hs @@ -43,77 +52,131 @@ def __init__(self, hs: "HomeServer"): self._event_auth_handler = hs.get_event_auth_handler() self._federation_client = hs.get_federation_client() - async def is_event_allowed(self, event: EventBase) -> bool: - """Check if the given event is allowed in the room by the policy server. + def _is_policy_server_state_event(self, event: EventBase) -> bool: + state_key = event.get_state_key() + if state_key is not None and state_key == "": + # TODO: Remove unstable MSC4284 support + # https://github.com/element-hq/synapse/issues/19502 + # Note: we can probably drop this whole function when we remove unstable support + return event.type in [EventTypes.RoomPolicy, "org.matrix.msc4284.policy"] + return False - Note: This will *always* return True if the room's policy server is Synapse - itself. This is because Synapse can't be a policy server (currently). + async def _get_policy_server(self, room_id: str) -> PolicyServerInfo | None: + """Get the policy server's name and Ed25519 public key for the room, if set. - If no policy server is configured in the room, this returns True. Similarly, if - the policy server is invalid in any way (not joined, not a server, etc), this - returns True. + If the `m.room.policy` state event is invalid, then a policy server is not set. It + can be invalid if: + - The room doesn't have an `m.room.policy` state event with empty state key. + - The policy state event is missing the `via` or `public_keys` field. + - The policy state event's public keys is missing an `ed25519` key. + - The via server is not a valid server name. + - The via server is not in the room. + - The via server is Synapse itself. - If a valid and contactable policy server is configured in the room, this returns - True if that server suggests the event is not spammy, and False otherwise. + TODO: Remove unstable MSC4284 support - https://github.com/element-hq/synapse/issues/19502 + This function also checks for the unstable `org.matrix.msc4284.policy` state event. Args: - event: The event to check. This should be a fully-formed PDU. + room_id: The room ID to get the policy server for. Returns: - bool: True if the event is allowed in the room, False otherwise. + A tuple of policy server name and its Ed25519 public key (unpadded base64). + Both values will be None if no policy server is configured or the configration + is invalid. """ - if event.type == POLICY_SERVER_EVENT_TYPE and event.state_key is not None: - return True # always allow policy server change events - policy_event = await self._storage_controllers.state.get_current_state_event( - event.room_id, POLICY_SERVER_EVENT_TYPE, "" + room_id, EventTypes.RoomPolicy, "" ) + public_key = None if not policy_event: - return True # no policy server == default allow + # TODO: Remove unstable MSC4284 support + # https://github.com/element-hq/synapse/issues/19502 + policy_event = ( + await self._storage_controllers.state.get_current_state_event( + room_id, "org.matrix.msc4284.policy", "" + ) + ) + if not policy_event: + return None # neither stable or unstable configured + + # Unstable configured, grab its public key + public_key = policy_event.content.get("public_key", None) + else: + # Stable configured, grab its public key + public_keys = policy_event.content.get("public_keys") + if isinstance(public_keys, dict): + ed25519_key = public_keys.get("ed25519") + if isinstance(ed25519_key, str): + public_key = ed25519_key + + if public_key is None or not isinstance(public_key, str): + return None # no public key means no policy server policy_server = policy_event.content.get("via", "") if policy_server is None or not isinstance(policy_server, str): - return True # no policy server == default allow + return None # no policy server if policy_server == self._hs.hostname: - return True # Synapse itself can't be a policy server (currently) + return None # Synapse itself can't be a policy server (currently) try: parse_and_validate_server_name(policy_server) except ValueError: - return True # invalid policy server == default allow + return None # invalid policy server is_in_room = await self._event_auth_handler.is_host_in_room( - event.room_id, policy_server + room_id, policy_server ) if not is_in_room: - return True # policy server not in room == default allow - - # Check if the event has been signed with the public key in the policy server state event. - # If it is, we can save an HTTP hit. - # We actually want to get the policy server state event BEFORE THE EVENT rather than - # the current state value, else changing the public key will cause all of these checks to fail. - # However, if we are checking outlier events (which we will due to is_event_allowed being called - # near the edges at _check_sigs_and_hash) we won't know the state before the event, so the - # only safe option is to use the current state - public_key = policy_event.content.get("public_key", None) - if public_key is not None and isinstance(public_key, str): - valid = await self._verify_policy_server_signature( - event, policy_server, public_key - ) - if valid: - return True - # fallthrough to hit /check manually - - # At this point, the server appears valid and is in the room, so ask it to check - # the event. - recommendation = await self._federation_client.get_pdu_policy_recommendation( - policy_server, event + return None # policy server not in room + + return PolicyServerInfo(policy_server, public_key) + + async def is_event_allowed(self, event: EventBase) -> bool: + """Check if the given event is allowed in the room by the policy server. + + Note: This will *always* return True if the room's policy server is Synapse + itself. This is because Synapse can't be a policy server (currently). + + If no policy server is configured in the room, this returns True. Similarly, if + the policy server is invalid in any way (not joined, not a server, etc), this + returns True. + + If a valid and contactable policy server is configured in the room, this returns + True if that server suggests the event is not spammy, and False otherwise. + + Args: + event: The event to check. This should be a fully-formed PDU. + + Returns: + bool: True if the event is allowed in the room, False otherwise. + """ + if self._is_policy_server_state_event(event): + return True # always allow policy server change events + + policy_server = await self._get_policy_server(event.room_id) + if policy_server is None: + return True # no policy server configured, so allow + + # Check if the event has been signed with the public key in the policy server + # state event. If it is, the event is valid according to the policy server and + # we don't need to request a fresh signature. + valid = await self._verify_policy_server_signature( + event, policy_server.server_name, policy_server.public_key ) - if recommendation != RECOMMENDATION_OK: + if valid: + return True # valid signature == allow + + # We couldn't save the HTTP hit, so do that hit. + try: + await self.ask_policy_server_to_sign_event(event, verify=True) + except Exception as ex: + # We probably caught either a refusal to sign, an invalid signature, or + # some other transient or network error. These are all rejection cases. + logger.warning("Failed to get a signature from the policy server: %s", ex) return False - return True # default allow + return True # passed all verifications and checks, so allow async def _verify_policy_server_signature( self, event: EventBase, policy_server: str, public_key: str @@ -140,47 +203,71 @@ async def ask_policy_server_to_sign_event( """Ask the policy server to sign this event. The signature is added to the event signatures block. Does nothing if there is no policy server state event in the room. If the policy server - refuses to sign the event (as it's marked as spam) does nothing. + refuses to sign the event (as it's marked as spam), an error is raised. Args: - event: The event to sign - verify: If True, verify that the signature is correctly signed by the public_key in the - policy server state event. + event: The event to sign. + verify: If True, verify that the signature is correctly signed by the policy server's + defined public key. Raises: - if verify=True and the policy server signed the event with an invalid signature. Does - not raise if the policy server refuses to sign the event. + When the policy server refuses to sign the event, or when verify is True and the + signature is invalid. """ - policy_event = await self._storage_controllers.state.get_current_state_event( - event.room_id, POLICY_SERVER_EVENT_TYPE, "" - ) - if not policy_event: + if self._is_policy_server_state_event(event): + # per spec, policy servers aren't asked to sign `m.room.policy` state events + # with empty state keys return - policy_server = policy_event.content.get("via", None) - if policy_server is None or not isinstance(policy_server, str): - return - # Only ask to sign events if the policy state event has a public_key (so they can be subsequently verified) - public_key = policy_event.content.get("public_key", None) - if public_key is None or not isinstance(public_key, str): + + policy_server = await self._get_policy_server(event.room_id) + if policy_server is None: return # Ask the policy server to sign this event. # We set a smallish timeout here as we don't want to block event sending too long. - signature = await self._federation_client.ask_policy_server_to_sign_event( - policy_server, - event, - timeout=3000, - ) - if ( - # the policy server returns {} if it refuses to sign the event. - signature and len(signature) > 0 - ): - event.signatures.update(signature) - if verify: - is_valid = await self._verify_policy_server_signature( - event, policy_server, public_key + try: + signature = await self._federation_client.ask_policy_server_to_sign_event( + policy_server.server_name, + event, + timeout=3000, + ) + # TODO: We can *probably* remove this when we remove unstable MSC4284 support. + # The server *should* be returning either a signature or an error, but there could + # also be implementation bugs. Whoever reads this when removing unstable MSC4284 + # stuff, make a decision on whether to remove this bit. + # https://github.com/element-hq/synapse/issues/19502 + if not signature or len(signature) == 0: + raise SynapseError( + 403, + "This event has been rejected as probable spam by the policy server", + Codes.FORBIDDEN, + ) + + # Note: if the policy server and event sender are the same server, the sender + # might not have added policy server signatures to the event for whatever reason. + # When this happens, we don't want to obliterate the event's existing signatures + # because the event will fail authorization. This is why we add defaults rather + # than simply `update` the signatures on the event. + # + # This situation can happen if the homeserver and policy server parts are + # logically the same server, but run by different software. For example, Synapse + # will not ask "itself" for a policy server signature, even if its server name + # is the designated policy server, so it could send an event outwards that other + # servers need to manually fetch signatures for. This is the code that allows + # those events to continue working (because they're legally sent, even if missing + # the policy server signature). + event.signatures.setdefault(policy_server.server_name, {}).update( + signature.get(policy_server.server_name, {}) + ) + except HttpResponseException as ex: + # re-wrap HTTP errors as `SynapseError` so they can be proxied to clients directly + raise ex.to_synapse_error() from ex + + if verify: + is_valid = await self._verify_policy_server_signature( + event, policy_server.server_name, policy_server.public_key + ) + if not is_valid: + raise SynapseError( + 500, + f"policy server {policy_server.server_name} failed to sign event correctly", ) - if not is_valid: - raise SynapseError( - 500, - f"policy server {policy_server} failed to sign event correctly", - ) diff --git a/synapse/types/handlers/policy_server.py b/synapse/types/handlers/policy_server.py deleted file mode 100644 index bfc09dabf4d..00000000000 --- a/synapse/types/handlers/policy_server.py +++ /dev/null @@ -1,16 +0,0 @@ -# -# This file is licensed under the Affero General Public License (AGPL) version 3. -# -# Copyright (C) 2025 New Vector, Ltd -# -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU Affero General Public License as -# published by the Free Software Foundation, either version 3 of the -# License, or (at your option) any later version. -# -# See the GNU Affero General Public License for more details: -# . -# - -RECOMMENDATION_OK = "ok" -RECOMMENDATION_SPAM = "spam" diff --git a/tests/handlers/test_room_policy.py b/tests/handlers/test_room_policy.py index ff212ab06ef..9adf0e38c25 100644 --- a/tests/handlers/test_room_policy.py +++ b/tests/handlers/test_room_policy.py @@ -19,7 +19,8 @@ from twisted.internet.testing import MemoryReactor -from synapse.api.errors import SynapseError +from synapse.api.constants import EventTypes +from synapse.api.errors import HttpResponseException, SynapseError from synapse.crypto.event_signing import compute_event_signature from synapse.events import EventBase, make_event_from_dict from synapse.handlers.room_policy import POLICY_SERVER_KEY_ID @@ -27,7 +28,6 @@ from synapse.rest.client import filter, login, room, sync from synapse.server import HomeServer from synapse.types import JsonDict, UserID -from synapse.types.handlers.policy_server import RECOMMENDATION_OK, RECOMMENDATION_SPAM from synapse.util.clock import Clock from tests import unittest @@ -49,13 +49,9 @@ def make_homeserver(self, reactor: MemoryReactor, clock: Clock) -> HomeServer: # mock out the federation transport client self.mock_federation_transport_client = mock.Mock( spec=[ - "get_policy_recommendation_for_pdu", "ask_policy_server_to_sign_event", ] ) - self.mock_federation_transport_client.get_policy_recommendation_for_pdu = ( - mock.AsyncMock() - ) self.mock_federation_transport_client.ask_policy_server_to_sign_event = ( mock.AsyncMock() ) @@ -106,25 +102,6 @@ def prepare(self, reactor: MemoryReactor, clock: Clock, hs: HomeServer) -> None: }, ) - # Prepare the policy server mock to decide spam vs not spam on those events - self.call_count = 0 - - async def get_policy_recommendation_for_pdu( - destination: str, - pdu: EventBase, - timeout: int | None = None, - ) -> JsonDict: - self.call_count += 1 - self.assertEqual(destination, self.OTHER_SERVER_NAME) - if pdu.event_id == self.spammy_event.event_id: - return {"recommendation": RECOMMENDATION_SPAM} - elif pdu.event_id == self.not_spammy_event.event_id: - return {"recommendation": RECOMMENDATION_OK} - else: - self.fail("Unexpected event ID") - - self.mock_federation_transport_client.get_policy_recommendation_for_pdu.side_effect = get_policy_recommendation_for_pdu - # Mock policy server actions on signing events async def policy_server_signs_event( destination: str, pdu: EventBase, timeout: int | None = None @@ -157,7 +134,9 @@ async def policy_server_refuses_to_sign_event( async def policy_server_event_sign_error( destination: str, pdu: EventBase, timeout: int | None = None ) -> JsonDict | None: - return None + raise HttpResponseException( + 500, "Internal Server Error", b'{"errcode": "M_UNKNOWN"}' + ) self.policy_server_signs_event = policy_server_signs_event self.policy_server_refuses_to_sign_event = policy_server_refuses_to_sign_event @@ -174,14 +153,16 @@ def _add_policy_server_to_room(self, public_key: str | None = None) -> None: self.hs, self.room_id, policy_user_id, "join" ) ) - content = { + content: JsonDict = { "via": self.OTHER_SERVER_NAME, } if public_key is not None: - content["public_key"] = public_key + content["public_keys"] = { + "ed25519": public_key, + } self.helper.send_state( self.room_id, - "org.matrix.msc4284.policy", + EventTypes.RoomPolicy, content, tok=self.creator_token, state_key="", @@ -192,12 +173,11 @@ def test_no_policy_event_set(self) -> None: # case where a room doesn't use a policy server. ok = self.get_success(self.handler.is_event_allowed(self.spammy_event)) self.assertEqual(ok, True) - self.assertEqual(self.call_count, 0) def test_empty_policy_event_set(self) -> None: self.helper.send_state( self.room_id, - "org.matrix.msc4284.policy", + EventTypes.RoomPolicy, { # empty content (no `via`) }, @@ -207,12 +187,11 @@ def test_empty_policy_event_set(self) -> None: ok = self.get_success(self.handler.is_event_allowed(self.spammy_event)) self.assertEqual(ok, True) - self.assertEqual(self.call_count, 0) def test_nonstring_policy_event_set(self) -> None: self.helper.send_state( self.room_id, - "org.matrix.msc4284.policy", + EventTypes.RoomPolicy, { "via": 42, # should be a server name }, @@ -222,12 +201,11 @@ def test_nonstring_policy_event_set(self) -> None: ok = self.get_success(self.handler.is_event_allowed(self.spammy_event)) self.assertEqual(ok, True) - self.assertEqual(self.call_count, 0) def test_self_policy_event_set(self) -> None: self.helper.send_state( self.room_id, - "org.matrix.msc4284.policy", + EventTypes.RoomPolicy, { # We ignore events when the policy server is ourselves (for now?) "via": (UserID.from_string(self.creator)).domain, @@ -238,12 +216,11 @@ def test_self_policy_event_set(self) -> None: ok = self.get_success(self.handler.is_event_allowed(self.spammy_event)) self.assertEqual(ok, True) - self.assertEqual(self.call_count, 0) def test_invalid_server_policy_event_set(self) -> None: self.helper.send_state( self.room_id, - "org.matrix.msc4284.policy", + EventTypes.RoomPolicy, { "via": "|this| is *not* a (valid) server name.com", }, @@ -253,12 +230,11 @@ def test_invalid_server_policy_event_set(self) -> None: ok = self.get_success(self.handler.is_event_allowed(self.spammy_event)) self.assertEqual(ok, True) - self.assertEqual(self.call_count, 0) def test_not_in_room_policy_event_set(self) -> None: self.helper.send_state( self.room_id, - "org.matrix.msc4284.policy", + EventTypes.RoomPolicy, { "via": f"x.{self.OTHER_SERVER_NAME}", }, @@ -268,14 +244,30 @@ def test_not_in_room_policy_event_set(self) -> None: ok = self.get_success(self.handler.is_event_allowed(self.spammy_event)) self.assertEqual(ok, True) - self.assertEqual(self.call_count, 0) + + def test_missing_public_key_event_set(self) -> None: + """ + Tests that a missing public key in the `m.room.policy` state event (an invalid + configuration) is treated as though there is no policy server configured, thus + allowing all events. + """ + self._add_policy_server_to_room() # no public_key + + ok = self.get_success(self.handler.is_event_allowed(self.spammy_event)) + self.assertEqual(ok, True) def test_spammy_event_is_spam(self) -> None: - self._add_policy_server_to_room() + verify_key_str = encode_verify_key_base64(get_verify_key(self.signing_key)) + self._add_policy_server_to_room(public_key=verify_key_str) + + # Explicitly configure the policy server mock to refuse to sign the event. + self.mock_federation_transport_client.ask_policy_server_to_sign_event.return_value = False ok = self.get_success(self.handler.is_event_allowed(self.spammy_event)) self.assertEqual(ok, False) - self.assertEqual(self.call_count, 1) + + # Ensure we actually contacted the policy server once for this event. + self.mock_federation_transport_client.ask_policy_server_to_sign_event.assert_awaited_once() def test_signed_event_is_not_spam(self) -> None: verify_key_str = encode_verify_key_base64(get_verify_key(self.signing_key)) @@ -306,8 +298,6 @@ def test_signed_event_is_not_spam(self) -> None: ok = self.get_success(self.handler.is_event_allowed(event)) self.assertEqual(ok, True) - # Make sure we did not make an HTTP hit to get_policy_recommendation_for_pdu - self.assertEqual(self.call_count, 0) def test_ask_policy_server_to_sign_event_ok(self) -> None: verify_key_str = encode_verify_key_base64(get_verify_key(self.signing_key)) @@ -348,8 +338,15 @@ def test_ask_policy_server_to_sign_event_refuses(self) -> None: }, ) self.mock_federation_transport_client.ask_policy_server_to_sign_event.side_effect = self.policy_server_refuses_to_sign_event - self.get_success( - self.handler.ask_policy_server_to_sign_event(event, verify=True) + fail = self.get_failure( + self.handler.ask_policy_server_to_sign_event(event, verify=True), + SynapseError, + ) + self.assertIsInstance(fail.value, SynapseError) + self.assertEqual(fail.value.code, 403) + self.assertEqual( + fail.value.msg, + "This event has been rejected as probable spam by the policy server", ) self.assertEqual(len(event.signatures), 0) @@ -370,9 +367,12 @@ def test_ask_policy_server_to_sign_event_cannot_reach(self) -> None: }, ) self.mock_federation_transport_client.ask_policy_server_to_sign_event.side_effect = self.policy_server_event_sign_error - self.get_success( - self.handler.ask_policy_server_to_sign_event(event, verify=True) + fail = self.get_failure( + self.handler.ask_policy_server_to_sign_event(event, verify=True), + SynapseError, ) + self.assertIsInstance(fail.value, SynapseError) + self.assertEqual(fail.value.code, 500) self.assertEqual(len(event.signatures), 0) def test_ask_policy_server_to_sign_event_wrong_sig(self) -> None: From 713aa7ebf08a0a8db6caf34fa92204a1a357b5e4 Mon Sep 17 00:00:00 2001 From: Andrew Morgan <1342360+anoadragon453@users.noreply.github.com> Date: Mon, 23 Mar 2026 15:16:23 +0000 Subject: [PATCH 090/121] Hide successful, skipped Complement tests in the CI (#19590) Co-authored-by: Eric Eastwood --- .ci/complement_package.gotpl | 5 ++-- .ci/scripts/gotestfmt | 21 ---------------- .ci/scripts/setup_complement_prerequisites.sh | 1 - .github/workflows/tests.yml | 24 +++++++++++++++---- changelog.d/19590.misc | 1 + 5 files changed, 24 insertions(+), 28 deletions(-) delete mode 100755 .ci/scripts/gotestfmt create mode 100644 changelog.d/19590.misc diff --git a/.ci/complement_package.gotpl b/.ci/complement_package.gotpl index 2dd5e5e0e69..e71ed9f616b 100644 --- a/.ci/complement_package.gotpl +++ b/.ci/complement_package.gotpl @@ -29,7 +29,7 @@ which is under the Unlicense licence. {{- with .TestCases -}} {{- /* Passing tests are first */ -}} {{- range . -}} - {{- if eq .Result "PASS" -}} + {{- if and (eq .Result "PASS") (not $settings.HideSuccessfulTests) -}} ::group::{{ "\033" }}[0;32m✅{{ " " }}{{- .Name -}} {{- "\033" -}}[0;37m ({{if $settings.ShowTestStatus}}{{.Result}}; {{end}}{{ .Duration -}} {{- with .Coverage -}} @@ -49,7 +49,8 @@ which is under the Unlicense licence. {{- /* Then skipped tests are second */ -}} {{- range . -}} - {{- if eq .Result "SKIP" -}} + {{- /* Skipped tests are also purposefully hidden if "HideSuccessfulTests" is enabled */ -}} + {{- if and (eq .Result "SKIP") (not $settings.HideSuccessfulTests) -}} ::group::{{ "\033" }}[0;33m🚧{{ " " }}{{- .Name -}} {{- "\033" -}}[0;37m ({{if $settings.ShowTestStatus}}{{.Result}}; {{end}}{{ .Duration -}} {{- with .Coverage -}} diff --git a/.ci/scripts/gotestfmt b/.ci/scripts/gotestfmt deleted file mode 100755 index 83e0ec63613..00000000000 --- a/.ci/scripts/gotestfmt +++ /dev/null @@ -1,21 +0,0 @@ -#!/bin/bash -# -# wraps `gotestfmt`, hiding output from successful packages unless -# all tests passed. - -set -o pipefail -set -e - -# tee the test results to a log, whilst also piping them into gotestfmt, -# telling it to hide successful results, so that we can clearly see -# unsuccessful results. -tee complement.log | gotestfmt -hide successful-packages - -# gotestfmt will exit non-zero if there were any failures, so if we got to this -# point, we must have had a successful result. -echo "All tests successful; showing all test results" - -# Pipe the test results back through gotestfmt, showing all results. -# The log file consists of JSON lines giving the test results, interspersed -# with regular stdout lines (including reports of downloaded packages). -grep '^{"Time":' complement.log | gotestfmt diff --git a/.ci/scripts/setup_complement_prerequisites.sh b/.ci/scripts/setup_complement_prerequisites.sh index 47a3ff8e696..1fdaf5e631a 100755 --- a/.ci/scripts/setup_complement_prerequisites.sh +++ b/.ci/scripts/setup_complement_prerequisites.sh @@ -10,7 +10,6 @@ alias block='{ set +x; } 2>/dev/null; func() { echo "::group::$*"; set -x; }; fu alias endblock='{ set +x; } 2>/dev/null; func() { echo "::endgroup::"; set -x; }; func' block Install Complement Dependencies - sudo apt-get -qq update && sudo apt-get install -qqy libolm3 libolm-dev go install -v github.com/gotesttools/gotestfmt/v2/cmd/gotestfmt@latest endblock diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 35c633b8edf..c3dd8b14e56 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -744,6 +744,12 @@ jobs: - name: Formatted sanity check Complement test logs # Always run this step if we attempted to run the Complement tests. if: always() && steps.run_sanity_check_complement_image_test.outcome != 'skipped' + # We do not hide successful tests in `gotestfmt` here as the list of sanity + # check tests is so short. Feel free to change this when we get more tests. + # + # Note that the `-hide` argument is interpreted by `gotestfmt`. From it, + # it derives several values under `$settings` and passes them to our + # custom `.ci/complement_package.gotpl` template to render the output. run: cat /tmp/gotest-sanity-check-complement.log | gotestfmt -hide "successful-downloads,empty-packages" - name: Run Complement Tests @@ -764,10 +770,15 @@ jobs: POSTGRES: ${{ (matrix.database == 'Postgres') && 1 || '' }} WORKERS: ${{ (matrix.arrangement == 'workers') && 1 || '' }} - - name: Formatted Complement test logs + - name: Formatted Complement test logs (only failing are shown) # Always run this step if we attempted to run the Complement tests. if: always() && steps.run_complement_tests.outcome != 'skipped' - run: cat /tmp/gotest-complement.log | gotestfmt -hide "successful-downloads,empty-packages" + # Hide successful tests in order to reduce the verbosity of the otherwise very large output. + # + # Note that the `-hide` argument is interpreted by `gotestfmt`. From it, + # it derives several values under `$settings` and passes them to our + # custom `.ci/complement_package.gotpl` template to render the output. + run: cat /tmp/gotest-complement.log | gotestfmt -hide "successful-downloads,successful-tests,empty-packages" - name: Run in-repo Complement Tests id: run_in_repo_complement_tests @@ -787,10 +798,15 @@ jobs: POSTGRES: ${{ (matrix.database == 'Postgres') && 1 || '' }} WORKERS: ${{ (matrix.arrangement == 'workers') && 1 || '' }} - - name: Formatted in-repo Complement test logs + - name: Formatted in-repo Complement test logs (only failing are shown) # Always run this step if we attempted to run the Complement tests. if: always() && steps.run_in_repo_complement_tests.outcome != 'skipped' - run: cat /tmp/gotest-in-repo-complement.log | gotestfmt -hide "successful-downloads,empty-packages" + # Hide successful tests in order to reduce the verbosity of the otherwise very large output. + # + # Note that the `-hide` argument is interpreted by `gotestfmt`. From it, + # it derives several values under `$settings` and passes them to our + # custom `.ci/complement_package.gotpl` template to render the output. + run: cat /tmp/gotest-in-repo-complement.log | gotestfmt -hide "successful-downloads,successful-tests,empty-packages" cargo-test: if: ${{ needs.changes.outputs.rust == 'true' }} diff --git a/changelog.d/19590.misc b/changelog.d/19590.misc new file mode 100644 index 00000000000..b5357334ceb --- /dev/null +++ b/changelog.d/19590.misc @@ -0,0 +1 @@ +Only show failing Complement tests in the formatted output in CI. \ No newline at end of file From 963fc9e55ad2d8786e67a6d4a2407f806109186d Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 24 Mar 2026 10:16:28 +0000 Subject: [PATCH 091/121] Bump docker/metadata-action from 5.10.0 to 6.0.0 (#19600) --- .github/workflows/docker.yml | 2 +- .github/workflows/push_complement_image.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml index a796f9cf2a8..a54efb89371 100644 --- a/.github/workflows/docker.yml +++ b/.github/workflows/docker.yml @@ -189,7 +189,7 @@ jobs: uses: sigstore/cosign-installer@faadad0cce49287aee09b3a48701e75088a2c6ad # v4.0.0 - name: Calculate docker image tag - uses: docker/metadata-action@c299e40c65443455700f0fdfc63efafe5b349051 # v5.10.0 + uses: docker/metadata-action@030e881283bb7a6894de51c315a6bfe6a94e05cf # v6.0.0 with: images: ${{ matrix.repository }} flavor: | diff --git a/.github/workflows/push_complement_image.yml b/.github/workflows/push_complement_image.yml index 12599457dc7..e0384e7091d 100644 --- a/.github/workflows/push_complement_image.yml +++ b/.github/workflows/push_complement_image.yml @@ -59,7 +59,7 @@ jobs: password: ${{ secrets.GITHUB_TOKEN }} - name: Work out labels for complement image id: meta - uses: docker/metadata-action@c299e40c65443455700f0fdfc63efafe5b349051 # v5.10.0 + uses: docker/metadata-action@030e881283bb7a6894de51c315a6bfe6a94e05cf # v6.0.0 with: images: ghcr.io/${{ github.repository }}/complement-synapse tags: | From e0b08da42ee0079c03da55eb580d548a419cbe32 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 24 Mar 2026 10:16:50 +0000 Subject: [PATCH 092/121] Bump docker/build-push-action from 6.19.2 to 7.0.0 (#19599) --- .github/workflows/docker.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml index a54efb89371..5ccc6494039 100644 --- a/.github/workflows/docker.yml +++ b/.github/workflows/docker.yml @@ -87,7 +87,7 @@ jobs: - name: Build and push by digest id: build - uses: docker/build-push-action@10e90e3645eae34f1e60eeb005ba3a3d33f178e8 # v6.19.2 + uses: docker/build-push-action@d08e5c354a6adb9ed34480a06d141179aa583294 # v7.0.0 with: push: true labels: | From 71092aa58474f3a0488324cb50470c83bc41b21e Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 24 Mar 2026 10:17:14 +0000 Subject: [PATCH 093/121] Bump docker/setup-buildx-action from 3.12.0 to 4.0.0 (#19598) --- .github/workflows/docker.yml | 4 ++-- .github/workflows/release-artifacts.yml | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml index 5ccc6494039..3baad943844 100644 --- a/.github/workflows/docker.yml +++ b/.github/workflows/docker.yml @@ -28,7 +28,7 @@ jobs: steps: - name: Set up Docker Buildx id: buildx - uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3.12.0 + uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4.0.0 - name: Checkout repository uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 @@ -183,7 +183,7 @@ jobs: password: ${{ steps.import-secrets.outputs.OCI_PASSWORD }} - name: Set up Docker Buildx - uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3.12.0 + uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4.0.0 - name: Install Cosign uses: sigstore/cosign-installer@faadad0cce49287aee09b3a48701e75088a2c6ad # v4.0.0 diff --git a/.github/workflows/release-artifacts.yml b/.github/workflows/release-artifacts.yml index 125d659f4e1..95d3d98d75b 100644 --- a/.github/workflows/release-artifacts.yml +++ b/.github/workflows/release-artifacts.yml @@ -61,7 +61,7 @@ jobs: - name: Set up Docker Buildx id: buildx - uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3.12.0 + uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4.0.0 - name: Set up docker layer caching uses: actions/cache@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5.0.3 From 57ef7b91920f998d897422680c220b00d5a3fa7e Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 24 Mar 2026 10:17:30 +0000 Subject: [PATCH 094/121] Bump docker/login-action from 3.7.0 to 4.0.0 (#19597) --- .github/workflows/docker.yml | 12 ++++++------ .github/workflows/push_complement_image.yml | 2 +- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml index 3baad943844..d581b5ec566 100644 --- a/.github/workflows/docker.yml +++ b/.github/workflows/docker.yml @@ -41,13 +41,13 @@ jobs: echo "SYNAPSE_VERSION=$(grep "^version" pyproject.toml | sed -E 's/version\s*=\s*["]([^"]*)["]/\1/')" >> $GITHUB_ENV - name: Log in to DockerHub - uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3.7.0 + uses: docker/login-action@b45d80f862d83dbcd57f89517bcf500b2ab88fb2 # v4.0.0 with: username: ${{ secrets.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKERHUB_TOKEN }} - name: Log in to GHCR - uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3.7.0 + uses: docker/login-action@b45d80f862d83dbcd57f89517bcf500b2ab88fb2 # v4.0.0 with: registry: ghcr.io username: ${{ github.repository_owner }} @@ -79,7 +79,7 @@ jobs: services/backend-repositories/secret/data/oci.element.io password | OCI_PASSWORD ; - name: Login to Element OCI Registry - uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3.7.0 + uses: docker/login-action@b45d80f862d83dbcd57f89517bcf500b2ab88fb2 # v4.0.0 with: registry: oci-push.vpn.infra.element.io username: ${{ steps.import-secrets.outputs.OCI_USERNAME }} @@ -136,14 +136,14 @@ jobs: merge-multiple: true - name: Log in to DockerHub - uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3.7.0 + uses: docker/login-action@b45d80f862d83dbcd57f89517bcf500b2ab88fb2 # v4.0.0 if: ${{ startsWith(matrix.repository, 'docker.io') }} with: username: ${{ secrets.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKERHUB_TOKEN }} - name: Log in to GHCR - uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3.7.0 + uses: docker/login-action@b45d80f862d83dbcd57f89517bcf500b2ab88fb2 # v4.0.0 if: ${{ startsWith(matrix.repository, 'ghcr.io') }} with: registry: ghcr.io @@ -176,7 +176,7 @@ jobs: services/backend-repositories/secret/data/oci.element.io password | OCI_PASSWORD ; - name: Login to Element OCI Registry - uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3.7.0 + uses: docker/login-action@b45d80f862d83dbcd57f89517bcf500b2ab88fb2 # v4.0.0 with: registry: oci-push.vpn.infra.element.io username: ${{ steps.import-secrets.outputs.OCI_USERNAME }} diff --git a/.github/workflows/push_complement_image.yml b/.github/workflows/push_complement_image.yml index e0384e7091d..3931dbebb32 100644 --- a/.github/workflows/push_complement_image.yml +++ b/.github/workflows/push_complement_image.yml @@ -52,7 +52,7 @@ jobs: with: poetry-version: "2.2.1" - name: Login to registry - uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3.7.0 + uses: docker/login-action@b45d80f862d83dbcd57f89517bcf500b2ab88fb2 # v4.0.0 with: registry: ghcr.io username: ${{ github.actor }} From 4b19411445592f9ce109d35e2420f2a1f35610b5 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 24 Mar 2026 10:20:24 +0000 Subject: [PATCH 095/121] Bump rustls-webpki from 0.103.4 to 0.103.10 (#19594) --- Cargo.lock | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index d6945bfbb79..c15688814ca 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1116,9 +1116,9 @@ dependencies = [ [[package]] name = "rustls-webpki" -version = "0.103.4" +version = "0.103.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0a17884ae0c1b773f1ccd2bd4a8c72f16da897310a98b0e84bf349ad5ead92fc" +checksum = "df33b2b81ac578cabaf06b89b0631153a3f416b0a886e8a7a1707fb51abbd1ef" dependencies = [ "ring", "rustls-pki-types", From 33d47f43e441ef06ac5a855d383dc41df4f4dbdb Mon Sep 17 00:00:00 2001 From: Quentin Gliech Date: Tue, 24 Mar 2026 15:17:23 +0100 Subject: [PATCH 096/121] 1.150.0 --- CHANGES.md | 7 +++++++ debian/changelog | 6 ++++++ pyproject.toml | 2 +- 3 files changed, 14 insertions(+), 1 deletion(-) diff --git a/CHANGES.md b/CHANGES.md index e7c24162385..6dbee41e7cc 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,3 +1,10 @@ +# Synapse 1.150.0 (2026-03-24) + +No significant changes since 1.150.0rc1. + + + + # Synapse 1.150.0rc1 (2026-03-17) ## Features diff --git a/debian/changelog b/debian/changelog index 55af4c22b55..c8146764de1 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,3 +1,9 @@ +matrix-synapse-py3 (1.150.0) stable; urgency=medium + + * New synapse release 1.150.0. + + -- Synapse Packaging team Tue, 24 Mar 2026 14:17:04 +0000 + matrix-synapse-py3 (1.150.0~rc1) stable; urgency=medium [ Quentin Gliech ] diff --git a/pyproject.toml b/pyproject.toml index adb9993aae8..c2f01826876 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "matrix-synapse" -version = "1.150.0rc1" +version = "1.150.0" description = "Homeserver for the Matrix decentralised comms protocol" readme = "README.rst" authors = [ From 7fad50fd76fe1d6da6fa9154cdbb9e5e6acb7c4a Mon Sep 17 00:00:00 2001 From: Mathieu Velten Date: Tue, 24 Mar 2026 17:22:11 +0100 Subject: [PATCH 097/121] Limit outgoing to_device EDU size to 65536 (#18416) If a set of messages exceeds this limit, the messages are split across several EDUs. Fix #17035 (should) There is currently [no official specced limit for EDUs](https://github.com/matrix-org/matrix-spec/issues/807), but the consensus seems to be that it would be useful to have one to avoid this bug by bounding the transaction size. As a side effect it also limits the size of a single to-device message to a bit less than 65536. This should probably be added to the spec similarly to the [message size limit.](https://spec.matrix.org/v1.14/client-server-api/#size-limits) Spec PR: https://github.com/matrix-org/matrix-spec/pull/2340 --------- Co-authored-by: mcalinghee Co-authored-by: Eric Eastwood --- changelog.d/18416.bugfix | 1 + synapse/api/constants.py | 16 ++ .../sender/per_destination_queue.py | 13 +- synapse/handlers/devicemessage.py | 158 ++++++++++++++--- synapse/storage/databases/main/deviceinbox.py | 47 +++++- tests/handlers/test_appservice.py | 4 +- tests/rest/client/test_sendtodevice.py | 159 +++++++++++++++++- 7 files changed, 360 insertions(+), 38 deletions(-) create mode 100644 changelog.d/18416.bugfix diff --git a/changelog.d/18416.bugfix b/changelog.d/18416.bugfix new file mode 100644 index 00000000000..71f181fed40 --- /dev/null +++ b/changelog.d/18416.bugfix @@ -0,0 +1 @@ +A long queue of to-device messages could prevent outgoing federation because of the size of the transaction, let's limit the to-device EDU size to a reasonable value. diff --git a/synapse/api/constants.py b/synapse/api/constants.py index eb9e6cc39b9..27f06ec525b 100644 --- a/synapse/api/constants.py +++ b/synapse/api/constants.py @@ -30,6 +30,22 @@ # the max size of a (canonical-json-encoded) event MAX_PDU_SIZE = 65536 +# This isn't spec'ed but is our own reasonable default to play nice with Synapse's +# `max_request_size`/`max_request_body_size`. We chose the same as `MAX_PDU_SIZE` as our +# `max_request_body_size` math is currently limited by 200 `MAX_PDU_SIZE` things. The +# spec for a `/federation/v1/send` request sets the limit at 100 EDU's and 50 PDU's +# which is below that 200 `MAX_PDU_SIZE` limit (`max_request_body_size`). +# +# Allowing oversized EDU's results in failed `/federation/v1/send` transactions (because +# the request overall can overrun the `max_request_body_size`) which are retried over +# and over and prevent other outbound federation traffic from happening. +MAX_EDU_SIZE = 65536 + +# This is defined in the Matrix spec and enforced by the receiver. +MAX_EDUS_PER_TRANSACTION = 100 +# A transaction can contain up to 100 EDUs but synapse reserves 10 EDUs for other purposes +# like trickling out some device list updates. +NUMBER_OF_RESERVED_EDUS_PER_TRANSACTION = 10 # The maximum allowed size of an HTTP request. # Other than media uploads, the biggest request we expect to see is a fully-loaded diff --git a/synapse/federation/sender/per_destination_queue.py b/synapse/federation/sender/per_destination_queue.py index cdacf16d725..32f8630c9d1 100644 --- a/synapse/federation/sender/per_destination_queue.py +++ b/synapse/federation/sender/per_destination_queue.py @@ -30,7 +30,11 @@ from twisted.internet import defer -from synapse.api.constants import EduTypes +from synapse.api.constants import ( + MAX_EDUS_PER_TRANSACTION, + NUMBER_OF_RESERVED_EDUS_PER_TRANSACTION, + EduTypes, +) from synapse.api.errors import ( FederationDeniedError, HttpResponseException, @@ -51,9 +55,6 @@ if TYPE_CHECKING: import synapse.server -# This is defined in the Matrix spec and enforced by the receiver. -MAX_EDUS_PER_TRANSACTION = 100 - logger = logging.getLogger(__name__) @@ -798,7 +799,9 @@ async def __aenter__(self) -> tuple[list[EventBase], list[Edu]]: ( to_device_edus, device_stream_id, - ) = await self.queue._get_to_device_message_edus(edu_limit - 10) + ) = await self.queue._get_to_device_message_edus( + edu_limit - NUMBER_OF_RESERVED_EDUS_PER_TRANSACTION + ) if to_device_edus: self._device_stream_id = device_stream_id diff --git a/synapse/handlers/devicemessage.py b/synapse/handlers/devicemessage.py index 0ef14b31dad..2096b44f6ca 100644 --- a/synapse/handlers/devicemessage.py +++ b/synapse/handlers/devicemessage.py @@ -23,8 +23,15 @@ from http import HTTPStatus from typing import TYPE_CHECKING, Any -from synapse.api.constants import EduTypes, EventContentFields, ToDeviceEventTypes -from synapse.api.errors import Codes, SynapseError +from canonicaljson import encode_canonical_json + +from synapse.api.constants import ( + MAX_EDU_SIZE, + EduTypes, + EventContentFields, + ToDeviceEventTypes, +) +from synapse.api.errors import Codes, EventSizeError, SynapseError from synapse.api.ratelimiting import Ratelimiter from synapse.logging.context import run_in_background from synapse.logging.opentracing import ( @@ -35,7 +42,7 @@ ) from synapse.types import JsonDict, Requester, StreamKeyType, UserID, get_domain_from_id from synapse.util.json import json_encoder -from synapse.util.stringutils import random_string +from synapse.util.stringutils import random_string_insecure_fast if TYPE_CHECKING: from synapse.server import HomeServer @@ -222,6 +229,7 @@ async def send_device_message( set_tag(SynapseTags.TO_DEVICE_TYPE, message_type) set_tag(SynapseTags.TO_DEVICE_SENDER, sender_user_id) local_messages = {} + # Map from destination (server) -> recipient (user ID) -> device_id -> JSON message content remote_messages: dict[str, dict[str, dict[str, JsonDict]]] = {} for user_id, by_device in messages.items(): if not UserID.is_valid(user_id): @@ -277,28 +285,33 @@ async def send_device_message( destination = get_domain_from_id(user_id) remote_messages.setdefault(destination, {})[user_id] = by_device - context = get_active_span_text_map() - - remote_edu_contents = {} - for destination, messages in remote_messages.items(): - # The EDU contains a "message_id" property which is used for - # idempotence. Make up a random one. - message_id = random_string(16) - log_kv({"destination": destination, "message_id": message_id}) - - remote_edu_contents[destination] = { - "messages": messages, - "sender": sender_user_id, - "type": message_type, - "message_id": message_id, - "org.matrix.opentracing_context": json_encoder.encode(context), - } - - # Add messages to the database. + # Add local messages to the database. # Retrieve the stream id of the last-processed to-device message. - last_stream_id = await self.store.add_messages_to_device_inbox( - local_messages, remote_edu_contents + last_stream_id = ( + await self.store.add_local_messages_from_client_to_device_inbox( + local_messages + ) ) + for destination, messages in remote_messages.items(): + split_edus = split_device_messages_into_edus( + sender_user_id, message_type, messages + ) + for edu in split_edus: + edu["org.matrix.opentracing_context"] = json_encoder.encode( + get_active_span_text_map() + ) + # Add remote messages to the database. + last_stream_id = ( + await self.store.add_remote_messages_from_client_to_device_inbox( + {destination: edu} + ) + ) + log_kv( + { + "destination": destination, + "message_id": edu["message_id"], + } + ) # Notify listeners that there are new to-device messages to process, # handing them the latest stream id. @@ -397,3 +410,102 @@ async def get_events_for_dehydrated_device( "events": messages, "next_batch": f"d{stream_id}", } + + +def split_device_messages_into_edus( + sender_user_id: str, + message_type: str, + messages_by_user_then_device: dict[str, dict[str, JsonDict]], +) -> list[JsonDict]: + """ + This function takes many to-device messages and fits/splits them into several EDUs + as necessary. We split the messages up as the overall request can overrun the + `max_request_body_size` and prevent outbound federation traffic because of the size + of the transaction (cf. `MAX_EDU_SIZE`). + + Args: + sender_user_id: The user that is sending the to-device messages. + message_type: The type of to-device messages that are being sent. + messages_by_user_then_device: Dictionary of recipient user_id to recipient device_id to message. + + Returns: + Bin-packed list of EDU JSON content for the given to_device messages + + Raises: + EventSizeError: If a single to-device message is too large to fit into an EDU. + """ + split_edus_content: list[JsonDict] = [] + + # Convert messages dict to a list of (recipient, messages_by_device) pairs + message_items = list(messages_by_user_then_device.items()) + + while message_items: + edu_messages = {} + # Start by trying to fit all remaining messages + target_count = len(message_items) + + while target_count > 0: + # Take the first target_count messages + edu_messages = dict(message_items[:target_count]) + edu_content = create_new_to_device_edu_content( + sender_user_id, message_type, edu_messages + ) + # Let's add the whole EDU structure before testing the size + edu = { + "content": edu_content, + "edu_type": EduTypes.DIRECT_TO_DEVICE, + } + + if len(encode_canonical_json(edu)) <= MAX_EDU_SIZE: + # It fits! Add this EDU and remove these messages from the list + split_edus_content.append(edu_content) + message_items = message_items[target_count:] + + logger.debug( + "Created EDU with %d recipients from %s (message_id=%s), (total EDUs so far: %d)", + target_count, + sender_user_id, + edu_content["message_id"], + len(split_edus_content), + ) + break + else: + if target_count == 1: + # Single recipient's messages are too large, let's reject the client + # call with 413/`M_TOO_LARGE`, we expect this error to reach the + # client in the case of the /sendToDevice endpoint. + # + # 413 is currently an unspecced response for `/sendToDevice` but is + # probably the best thing we can do. + # https://github.com/matrix-org/matrix-spec/pull/2340 tracks adding + # this to the spec + recipient = message_items[0][0] + raise EventSizeError( + f"device message to {recipient} too large to fit in a single EDU", + unpersistable=True, + ) + + # Halve the number of messages and try again + target_count = target_count // 2 + + return split_edus_content + + +def create_new_to_device_edu_content( + sender_user_id: str, + message_type: str, + messages_by_user_then_device: dict[str, dict[str, JsonDict]], +) -> JsonDict: + """ + Create a new `m.direct_to_device` EDU `content` object with a unique message ID. + """ + # The EDU contains a "message_id" property which is used for + # idempotence. Make up a random one. + message_id = random_string_insecure_fast(16) + content = { + "sender": sender_user_id, + "type": message_type, + "message_id": message_id, + "messages": messages_by_user_then_device, + } + return content diff --git a/synapse/storage/databases/main/deviceinbox.py b/synapse/storage/databases/main/deviceinbox.py index fc61f46c1c5..9fed22c1b3f 100644 --- a/synapse/storage/databases/main/deviceinbox.py +++ b/synapse/storage/databases/main/deviceinbox.py @@ -739,18 +739,15 @@ def get_all_new_device_messages_txn( ) @trace - async def add_messages_to_device_inbox( + async def add_local_messages_from_client_to_device_inbox( self, local_messages_by_user_then_device: dict[str, dict[str, JsonDict]], - remote_messages_by_destination: dict[str, JsonDict], ) -> int: - """Used to send messages from this server. + """Queue local device messages that will be sent to devices of local users. Args: local_messages_by_user_then_device: Dictionary of recipient user_id to recipient device_id to message. - remote_messages_by_destination: - Dictionary of destination server_name to the EDU JSON to send. Returns: The new stream_id. @@ -766,6 +763,39 @@ def add_messages_txn( txn, stream_id, local_messages_by_user_then_device ) + async with self._to_device_msg_id_gen.get_next() as stream_id: + now_ms = self.clock.time_msec() + await self.db_pool.runInteraction( + "add_local_messages_from_client_to_device_inbox", + add_messages_txn, + now_ms, + stream_id, + ) + for user_id in local_messages_by_user_then_device.keys(): + self._device_inbox_stream_cache.entity_has_changed(user_id, stream_id) + + return self._to_device_msg_id_gen.get_current_token() + + @trace + async def add_remote_messages_from_client_to_device_inbox( + self, + remote_messages_by_destination: dict[str, JsonDict], + ) -> int: + """Queue device messages that will be sent to remote servers. + + Args: + remote_messages_by_destination: + Dictionary of destination server_name to the EDU JSON to send. + + Returns: + The new stream_id. + """ + + assert self._can_write_to_device + + def add_messages_txn( + txn: LoggingTransaction, now_ms: int, stream_id: int + ) -> None: # Add the remote messages to the federation outbox. # We'll send them to a remote server when we next send a # federation transaction to that destination. @@ -824,10 +854,11 @@ def add_messages_txn( async with self._to_device_msg_id_gen.get_next() as stream_id: now_ms = self.clock.time_msec() await self.db_pool.runInteraction( - "add_messages_to_device_inbox", add_messages_txn, now_ms, stream_id + "add_remote_messages_from_client_to_device_inbox", + add_messages_txn, + now_ms, + stream_id, ) - for user_id in local_messages_by_user_then_device.keys(): - self._device_inbox_stream_cache.entity_has_changed(user_id, stream_id) for destination in remote_messages_by_destination.keys(): self._device_federation_outbox_stream_cache.entity_has_changed( destination, stream_id diff --git a/tests/handlers/test_appservice.py b/tests/handlers/test_appservice.py index 6336edb108a..ac7411291bc 100644 --- a/tests/handlers/test_appservice.py +++ b/tests/handlers/test_appservice.py @@ -856,7 +856,9 @@ def test_application_services_receive_bursts_of_to_device(self) -> None: # Seed the device_inbox table with our fake messages self.get_success( - self.hs.get_datastores().main.add_messages_to_device_inbox(messages, {}) + self.hs.get_datastores().main.add_local_messages_from_client_to_device_inbox( + messages + ) ) # Now have local_user send a final to-device message to exclusive_as_user. All unsent diff --git a/tests/rest/client/test_sendtodevice.py b/tests/rest/client/test_sendtodevice.py index 56533d85f58..526d9f0b3c4 100644 --- a/tests/rest/client/test_sendtodevice.py +++ b/tests/rest/client/test_sendtodevice.py @@ -18,9 +18,21 @@ # [This file includes modifications made by New Vector Limited] # # -from synapse.api.constants import EduTypes +from unittest.mock import AsyncMock, Mock + +from twisted.test.proto_helpers import MemoryReactor + +from synapse.api.constants import ( + MAX_EDU_SIZE, + MAX_EDUS_PER_TRANSACTION, + EduTypes, +) +from synapse.api.errors import Codes from synapse.rest import admin from synapse.rest.client import login, sendtodevice, sync +from synapse.server import HomeServer +from synapse.types import JsonDict +from synapse.util.clock import Clock from tests.unittest import HomeserverTestCase, override_config @@ -41,6 +53,20 @@ class SendToDeviceTestCase(HomeserverTestCase): sync.register_servlets, ] + def default_config(self) -> JsonDict: + config = super().default_config() + config["federation_sender_instances"] = None + return config + + def make_homeserver(self, reactor: MemoryReactor, clock: Clock) -> HomeServer: + self.federation_transport_client = Mock(spec=["send_transaction"]) + self.federation_transport_client.send_transaction = AsyncMock() + hs = self.setup_test_homeserver( + federation_transport_client=self.federation_transport_client, + ) + + return hs + def test_user_to_user(self) -> None: """A to-device message from one user to another should get delivered""" @@ -91,6 +117,137 @@ def test_user_to_user(self) -> None: self.assertEqual(channel.code, 200, channel.result) self.assertEqual(channel.json_body.get("to_device", {}).get("events", []), []) + def test_large_remote_todevice(self) -> None: + """A to-device message needs to fit in the EDU size limit""" + _ = self.register_user("u1", "pass") + user1_tok = self.login("u1", "pass", "d1") + + # Create a message that is over the `MAX_EDU_SIZE` + test_msg = {"foo": "a" * MAX_EDU_SIZE} + channel = self.make_request( + "PUT", + "/_matrix/client/r0/sendToDevice/m.test/12345", + content={"messages": {"@remote_user:secondserver": {"device": test_msg}}}, + access_token=user1_tok, + ) + self.assertEqual(channel.code, 413, channel.result) + self.assertEqual(Codes.TOO_LARGE, channel.json_body["errcode"]) + + def test_edu_large_messages_splitting(self) -> None: + """ + Test that a bunch of to-device messages are split over multiple EDUs if they are + collectively too large to fit into a single EDU + """ + mock_send_transaction: AsyncMock = ( + self.federation_transport_client.send_transaction + ) + mock_send_transaction.return_value = {} + + sender = self.hs.get_federation_sender() + + _ = self.register_user("u1", "pass") + user1_tok = self.login("u1", "pass", "d1") + destination = "secondserver" + messages = {} + + # 2 messages, each just big enough to fit into their own EDU + for i in range(2): + messages[f"@remote_user{i}:" + destination] = { + "device": {"foo": "a" * (MAX_EDU_SIZE - 1000)} + } + + channel = self.make_request( + "PUT", + "/_matrix/client/r0/sendToDevice/m.test/1234567", + content={"messages": messages}, + access_token=user1_tok, + ) + self.assertEqual(channel.code, 200, channel.result) + + self.get_success(sender.send_device_messages([destination])) + + number_of_edus_sent = 0 + for call in mock_send_transaction.call_args_list: + number_of_edus_sent += len(call[0][1]()["edus"]) + + self.assertEqual(number_of_edus_sent, 2) + + def test_edu_small_messages_not_splitting(self) -> None: + """ + Test that a couple of small messages do not get split into multiple EDUs + """ + mock_send_transaction: AsyncMock = ( + self.federation_transport_client.send_transaction + ) + mock_send_transaction.return_value = {} + + sender = self.hs.get_federation_sender() + + _ = self.register_user("u1", "pass") + user1_tok = self.login("u1", "pass", "d1") + destination = "secondserver" + messages = {} + + # 2 small messages that should fit in a single EDU + for i in range(2): + messages[f"@remote_user{i}:" + destination] = {"device": {"foo": "a" * 100}} + + channel = self.make_request( + "PUT", + "/_matrix/client/r0/sendToDevice/m.test/123456", + content={"messages": messages}, + access_token=user1_tok, + ) + self.assertEqual(channel.code, 200, channel.result) + + self.get_success(sender.send_device_messages([destination])) + + number_of_edus_sent = 0 + for call in mock_send_transaction.call_args_list: + number_of_edus_sent += len(call[0][1]()["edus"]) + + self.assertEqual(number_of_edus_sent, 1) + + def test_transaction_splitting(self) -> None: + """Test that a bunch of to-device messages are split into multiple transactions if there are too many EDUs""" + mock_send_transaction: AsyncMock = ( + self.federation_transport_client.send_transaction + ) + mock_send_transaction.return_value = {} + + sender = self.hs.get_federation_sender() + + _ = self.register_user("u1", "pass") + user1_tok = self.login("u1", "pass", "d1") + destination = "secondserver" + messages = {} + + number_of_edus_to_send = MAX_EDUS_PER_TRANSACTION + 1 + + for i in range(number_of_edus_to_send): + messages[f"@remote_user{i}:" + destination] = { + "device": {"foo": "a" * (MAX_EDU_SIZE - 1000)} + } + + channel = self.make_request( + "PUT", + "/_matrix/client/r0/sendToDevice/m.test/12345678", + content={"messages": messages}, + access_token=user1_tok, + ) + self.assertEqual(channel.code, 200, channel.result) + + self.get_success(sender.send_device_messages([destination])) + + # At least 2 transactions should be sent since we are over the EDU limit per transaction + self.assertGreaterEqual(mock_send_transaction.call_count, 2) + + number_of_edus_sent = 0 + for call in mock_send_transaction.call_args_list: + number_of_edus_sent += len(call[0][1]()["edus"]) + + self.assertEqual(number_of_edus_sent, number_of_edus_to_send) + @override_config({"rc_key_requests": {"per_second": 10, "burst_count": 2}}) def test_local_room_key_request(self) -> None: """m.room_key_request has special-casing; test from local user""" From 6c7e05fe206d8b9f5738bb4fc5712d1725651c59 Mon Sep 17 00:00:00 2001 From: Olivier 'reivilibre Date: Tue, 24 Mar 2026 17:39:21 +0000 Subject: [PATCH 098/121] Allow Synapse to start up even when discovery fails for an OpenID Connect provider. (#19509) Fixes: #8088 Previously we would perform OIDC discovery on startup, which involves making HTTP requests to the identity provider(s). If that took a long time, we would block startup. If that failed, we would crash startup. This commit: - makes the loading happen in the background on startup - makes an error in the 'preload' non-fatal (though it logs at CRITICAL for visibility) - adds a templated error page to show on failed redirects (for unavailable providers), as otherwise you get a JSON response in your navigator. - This involves introducing 2 new exception types to mark other exceptions and keep the error handling fine-grained. The machinery was already there to load-on-demand the discovery config, so when the identity provider comes back up, the discovery is reattempted and login can succeed. Signed-off-by: Olivier 'reivilibre --- changelog.d/19509.bugfix | 1 + complement/go.mod | 2 +- complement/tests/internal/dockerutil/files.go | 54 ++++++++ complement/tests/oidc_test.go | 117 ++++++++++++++++++ .../complement/conf/start_for_complement.sh | 4 + .../conf-workers/synapse.supervisord.conf.j2 | 9 ++ docker/configure_workers_and_start.py | 7 ++ synapse/app/_base.py | 8 +- synapse/handlers/oidc.py | 94 +++++++++++--- synapse/handlers/sso.py | 9 ++ synapse/res/templates/sso_error.html | 21 ++++ synapse/rest/client/login.py | 28 +++-- tests/handlers/test_oidc.py | 62 +++++++--- tests/server.py | 10 ++ 14 files changed, 379 insertions(+), 47 deletions(-) create mode 100644 changelog.d/19509.bugfix create mode 100644 complement/tests/internal/dockerutil/files.go create mode 100644 complement/tests/oidc_test.go diff --git a/changelog.d/19509.bugfix b/changelog.d/19509.bugfix new file mode 100644 index 00000000000..4c98f1f4ee9 --- /dev/null +++ b/changelog.d/19509.bugfix @@ -0,0 +1 @@ +Allow Synapse to start up even when discovery fails for an OpenID Connect provider. \ No newline at end of file diff --git a/complement/go.mod b/complement/go.mod index 2a9adb9b955..716aabdf321 100644 --- a/complement/go.mod +++ b/complement/go.mod @@ -10,7 +10,7 @@ require ( ) require ( - github.com/docker/docker v28.3.3+incompatible // indirect + github.com/docker/docker v28.3.3+incompatible github.com/docker/go-connections v0.5.0 // indirect github.com/hashicorp/go-set/v3 v3.0.0 // indirect github.com/moby/sys/atomicwriter v0.1.0 // indirect diff --git a/complement/tests/internal/dockerutil/files.go b/complement/tests/internal/dockerutil/files.go new file mode 100644 index 00000000000..62d2c557afb --- /dev/null +++ b/complement/tests/internal/dockerutil/files.go @@ -0,0 +1,54 @@ +package dockerutil + +import ( + "archive/tar" + "bytes" + "fmt" + "testing" + + "github.com/docker/docker/api/types/container" + "github.com/docker/docker/client" +) + +// Write `data` as a file into a container at the given `path`. +// +// Internally, produces an uncompressed single-file tape archive (tar) that is sent to the docker +// daemon to be unpacked into the container filesystem. +func WriteFileIntoContainer(t *testing.T, docker *client.Client, containerID string, path string, data []byte) error { + // Create a fake/virtual tar file in memory that we can copy to the container + // via https://stackoverflow.com/a/52131297/796832 + var buf bytes.Buffer + tw := tar.NewWriter(&buf) + err := tw.WriteHeader(&tar.Header{ + Name: path, + Mode: 0777, + Size: int64(len(data)), + }) + if err != nil { + return fmt.Errorf("WriteIntoContainer: failed to write tarball header for %s: %v", path, err) + } + _, err = tw.Write([]byte(data)) + if err != nil { + return fmt.Errorf("WriteIntoContainer: failed to write tarball data for %s: %w", path, err) + } + + err = tw.Close() + if err != nil { + return fmt.Errorf("WriteIntoContainer: failed to close tarball writer for %s: %w", path, err) + } + + // Put our new fake file in the container volume + err = docker.CopyToContainer( + t.Context(), + containerID, + "/", + &buf, + container.CopyToContainerOptions{ + AllowOverwriteDirWithFile: false, + }, + ) + if err != nil { + return fmt.Errorf("WriteIntoContainer: failed to copy: %s", err) + } + return nil +} diff --git a/complement/tests/oidc_test.go b/complement/tests/oidc_test.go new file mode 100644 index 00000000000..bddb382058b --- /dev/null +++ b/complement/tests/oidc_test.go @@ -0,0 +1,117 @@ +// This file is licensed under the Affero General Public License (AGPL) version 3. +// +// Copyright (C) 2026 Element Creations Ltd +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as +// published by the Free Software Foundation, either version 3 of the +// License, or (at your option) any later version. +// +// See the GNU Affero General Public License for more details: +// . + +package synapse_tests + +import ( + "net/http" + "net/url" + "strings" + "testing" + + dockerClient "github.com/docker/docker/client" + "github.com/element-hq/synapse/tests/internal/dockerutil" + "github.com/matrix-org/complement" + "github.com/matrix-org/complement/client" + "github.com/matrix-org/complement/match" + "github.com/matrix-org/complement/must" +) + +const OIDC_HOMESERVER_CONFIG string = ` +oidc_providers: + - idp_id: "test_provider" + idp_name: "Test OIDC Provider" + issuer: "https://example.invalid" + client_id: "test_client_id" + client_secret: "test_secret" + scopes: ["openid"] + discover: true + user_mapping_provider: + module: "synapse.handlers.oidc.JinjaOidcMappingProvider" + config: + display_name_template: "{{ user.given_name }}" + email_template: "{{ user.email }}" +` + +// Test that Synapse still starts up when configured with an OIDC provider that is unavailable. +// +// This is a regression test: Synapse previously would fail to start up +// at all if the OIDC provider was down on startup. +// https://github.com/element-hq/synapse/issues/8088 +// +// Now instead of failing to start, Synapse will produce a 503 response on the +// `/_matrix/client/v3/login/sso/redirect/oidc-test_provider` endpoint. +func TestOIDCProviderUnavailable(t *testing.T) { + // Deploy a single homeserver + deployment := complement.Deploy(t, 1) + defer deployment.Destroy(t) + + // Get Docker client to manipulate container + dc, err := dockerClient.NewClientWithOpts( + dockerClient.FromEnv, + dockerClient.WithAPIVersionNegotiation(), + ) + must.NotError(t, "failed creating docker client", err) + + // Configure the OIDC Provider by writing a config fragment + err = dockerutil.WriteFileIntoContainer( + t, + dc, + deployment.ContainerID(t, "hs1"), + "/conf/homeserver.d/oidc_provider.yaml", + []byte(OIDC_HOMESERVER_CONFIG), + ) + if err != nil { + t.Fatalf("Failed to write updated config to container: %v", err) + } + + // Restart the homeserver to apply the new config + deployment.StopServer(t, "hs1") + // Careful: port number changes here + deployment.StartServer(t, "hs1") + // Must get after the restart so the port number is correct + unauthedClient := deployment.UnauthenticatedClient(t, "hs1") + + // Test that trying to log in with an OIDC provider that is down + // causes an HTML error page to be shown to the user. + // (This replaces the redirect that would happen if the provider was + // up.) + // + // More importantly, implicitly tests that Synapse can start up + // and answer requests even though an OIDC provider is down. + t.Run("/login/sso/redirect shows HTML error", func(t *testing.T) { + // Build a request to the /redirect/ endpoint, that would normally be navigated to + // by the user's browser in order to start the login flow. + queryParams := url.Values{} + queryParams.Add("redirectUrl", "http://redirect.invalid/redirect") + res := unauthedClient.Do(t, "GET", []string{"_matrix", "client", "v3", "login", "sso", "redirect", "oidc-test_provider"}, + client.WithQueries(queryParams), + ) + + body := must.MatchResponse(t, res, match.HTTPResponse{ + // Should get a 503 + StatusCode: http.StatusServiceUnavailable, + + Headers: map[string]string{ + // Should get an HTML page explaining the problem to the user + "Content-Type": "text/html; charset=utf-8", + }, + }) + + bodyText := string(body) + + // The HTML page contains phrases from the template we expect + if !strings.Contains(bodyText, "login provider is unavailable right now") { + t.Fatalf("Keyword not found in HTML error page, got %s", bodyText) + } + }) +} diff --git a/docker/complement/conf/start_for_complement.sh b/docker/complement/conf/start_for_complement.sh index da1b26a2836..e0d30abed36 100755 --- a/docker/complement/conf/start_for_complement.sh +++ b/docker/complement/conf/start_for_complement.sh @@ -128,6 +128,10 @@ openssl x509 -req -in /conf/server.tls.csr \ export SYNAPSE_TLS_CERT=/conf/server.tls.crt export SYNAPSE_TLS_KEY=/conf/server.tls.key +# Add a directory for tests to add config overrides if they want +mkdir --parents /conf/homeserver.d +export _SYNAPSE_COMPLEMENT_EXTRA_CONFIG_DIR=/conf/homeserver.d + # Run the script that writes the necessary config files and starts supervisord, which in turn # starts everything else exec /configure_workers_and_start.py "$@" diff --git a/docker/conf-workers/synapse.supervisord.conf.j2 b/docker/conf-workers/synapse.supervisord.conf.j2 index 4fb11b259e5..9388b1ff0ef 100644 --- a/docker/conf-workers/synapse.supervisord.conf.j2 +++ b/docker/conf-workers/synapse.supervisord.conf.j2 @@ -5,10 +5,16 @@ command=/usr/local/bin/python -m synapse.app.complement_fork_starter {{ main_config_path }} synapse.app.homeserver --config-path="{{ main_config_path }}" + {%- if extra_config_dir %} + --config-path="{{ extra_config_dir }}" + {%- endif %} --config-path=/conf/workers/shared.yaml {%- for worker in workers %} -- {{ worker.app }} --config-path="{{ main_config_path }}" + {%- if extra_config_dir %} + --config-path="{{ extra_config_dir }}" + {%- endif %} --config-path=/conf/workers/shared.yaml --config-path=/conf/workers/{{ worker.name }}.yaml {%- endfor %} @@ -24,6 +30,9 @@ exitcodes=0 environment=http_proxy="%(ENV_SYNAPSE_HTTP_PROXY)s",https_proxy="%(ENV_SYNAPSE_HTTPS_PROXY)s",no_proxy="%(ENV_SYNAPSE_NO_PROXY)s" command=/usr/local/bin/prefix-log /usr/local/bin/python -m synapse.app.homeserver --config-path="{{ main_config_path }}" + {%- if extra_config_dir %} + --config-path="{{ extra_config_dir }}" + {%- endif %} --config-path=/conf/workers/shared.yaml priority=10 # Log startup failures to supervisord's stdout/err diff --git a/docker/configure_workers_and_start.py b/docker/configure_workers_and_start.py index ed15e8efef5..1b8d4f9989e 100755 --- a/docker/configure_workers_and_start.py +++ b/docker/configure_workers_and_start.py @@ -856,6 +856,7 @@ def parse_worker_types( def generate_worker_files( environ: Mapping[str, str], config_path: str, + extra_config_dir: str | None, data_dir: str, requested_workers: list[Worker], ) -> None: @@ -865,6 +866,7 @@ def generate_worker_files( Args: environ: os.environ instance. config_path: The location of the generated Synapse main worker config file. + extra_config_dir: A directory in which extra Synapse configuration files will be loaded. data_dir: The location of the synapse data directory. Where log and user-facing config files live. requested_workers: A list of requested workers @@ -1258,6 +1260,7 @@ def generate_worker_files( "/etc/supervisor/conf.d/synapse.conf", workers=worker_descriptors, main_config_path=config_path, + extra_config_dir=extra_config_dir, use_forking_launcher=environ.get("SYNAPSE_USE_EXPERIMENTAL_FORKING_LAUNCHER"), ) @@ -1318,6 +1321,9 @@ def main(args: list[str], environ: MutableMapping[str, str]) -> None: config_dir = environ.get("SYNAPSE_CONFIG_DIR", "/data") config_path = environ.get("SYNAPSE_CONFIG_PATH", config_dir + "/homeserver.yaml") + # All files in this directory will be loaded as `homeserver.yaml` snippets + # Currently only intended for use by Complement + extra_config_dir = environ.get("_SYNAPSE_COMPLEMENT_EXTRA_CONFIG_DIR", None) data_dir = environ.get("SYNAPSE_DATA_DIR", "/data") # override SYNAPSE_NO_TLS, we don't support TLS in worker mode, @@ -1352,6 +1358,7 @@ def main(args: list[str], environ: MutableMapping[str, str]) -> None: generate_worker_files( environ=environ, config_path=config_path, + extra_config_dir=extra_config_dir, data_dir=data_dir, requested_workers=requested_workers, ) diff --git a/synapse/app/_base.py b/synapse/app/_base.py index 7f4855f36bd..55df4c382e6 100644 --- a/synapse/app/_base.py +++ b/synapse/app/_base.py @@ -700,12 +700,10 @@ async def start(hs: "HomeServer", *, freeze: bool = True) -> None: # Load the OIDC provider metadatas, if OIDC is enabled. if hs.config.oidc.oidc_enabled: oidc = hs.get_oidc_handler() + # Preload the provider metadata. + # This will spawn fire-and-forget background processes. # Loading the provider metadata also ensures the provider config is valid. - # - # FIXME: It feels a bit strange to validate and block on startup as one of these - # OIDC providers could be temporarily unavailable and cause Synapse to be unable - # to start. - await oidc.load_metadata() + oidc.preload_metadata() # Load the certificate from disk. refresh_certificate(hs) diff --git a/synapse/handlers/oidc.py b/synapse/handlers/oidc.py index 429a7393800..09b19056a72 100644 --- a/synapse/handlers/oidc.py +++ b/synapse/handlers/oidc.py @@ -49,18 +49,20 @@ MacaroonInvalidSignatureException, ) +from twisted.internet import defer +from twisted.internet.defer import Deferred from twisted.web.client import readBody from twisted.web.http_headers import Headers from synapse.api.errors import SynapseError from synapse.config import ConfigError from synapse.config.oidc import OidcProviderClientSecretJwtKey, OidcProviderConfig -from synapse.handlers.sso import MappingException, UserAttributes +from synapse.handlers.sso import MappingException, SsoSetupError, UserAttributes from synapse.http.server import finish_request from synapse.http.servlet import parse_string from synapse.http.site import SynapseRequest from synapse.logging.context import make_deferred_yieldable -from synapse.module_api import ModuleApi +from synapse.metrics.background_process_metrics import wrap_as_background_process from synapse.types import JsonDict, UserID, map_username_to_mxid_localpart from synapse.util.caches.cached_call import RetryOnExceptionCachedCall from synapse.util.clock import Clock @@ -69,6 +71,7 @@ from synapse.util.templates import _localpart_from_email_filter if TYPE_CHECKING: + from synapse.module_api import ModuleApi from synapse.server import HomeServer logger = logging.getLogger(__name__) @@ -123,6 +126,8 @@ class OidcHandler: def __init__(self, hs: "HomeServer"): self._sso_handler = hs.get_sso_handler() + # Needed for wrap_as_background_process + self.hs = hs provider_confs = hs.config.oidc.oidc_providers # we should not have been instantiated if there is no configured provider. @@ -134,20 +139,47 @@ def __init__(self, hs: "HomeServer"): for p in provider_confs } - async def load_metadata(self) -> None: - """Validate the config and load the metadata from the remote endpoint. + @wrap_as_background_process("preload_oidc_metadata") + async def _preload_metadata_one_provider( + self, idp_id: str, p: "OidcProvider" + ) -> None: + """Attempt to preload the metadata from a single OIDC provider's remote endpoint + in the background. - Called at startup to ensure we have everything we need. + Will not raise exceptions, but will log at CRITICAL if an OIDC provider is broken. """ + + logger.info("Preloading OIDC provider %r", idp_id) + try: + await p.load_metadata() + if not p._uses_userinfo: + await p.load_jwks() + except Exception: + logger.critical( + # Include 'login' keyword for searchability. + "Error while preloading OIDC provider %r. Login may be broken!", + idp_id, + exc_info=True, + ) + + def preload_metadata(self) -> "Deferred[list[tuple[bool, None]]]": + """Attempt to preload the metadata from all the OIDC providers' remote endpoints + in the background. + + Will not raise exceptions, but will log at CRITICAL if an OIDC provider is broken. + + Can be **optionally** awaited in which case it will resolve when all + preloads are finished. + """ + + to_wait = [] for idp_id, p in self._providers.items(): - try: - await p.load_metadata() - if not p._uses_userinfo: - await p.load_jwks() - except Exception as e: - raise Exception( - "Error while initialising OIDC provider %r" % (idp_id,) - ) from e + to_wait.append(self._preload_metadata_one_provider(idp_id, p)) + + return defer.DeferredList( + to_wait, + consumeErrors=True, + ) async def handle_oidc_callback(self, request: SynapseRequest) -> None: """Handle an incoming request to /_synapse/client/oidc/callback @@ -359,6 +391,18 @@ def __str__(self) -> str: return self.error +class OidcDiscoveryError(SsoSetupError): + """ + Used to catch and mark errors when performing OIDC discovery. + """ + + +class OidcMetadataError(SsoSetupError): + """ + Used to catch and mark errors in the OIDC metadata configuration. + """ + + class OidcProvider: """Wraps the config for a single OIDC IdentityProvider @@ -372,6 +416,9 @@ def __init__( macaroon_generator: MacaroonGenerator, provider: OidcProviderConfig, ): + # Needed here to break import loops + from synapse.module_api import ModuleApi + self._store = hs.get_datastores().main self._clock = hs.get_clock() @@ -644,9 +691,15 @@ async def _load_metadata(self) -> OpenIDProviderMetadata: # load any data from the discovery endpoint, if enabled if self._config.discover: - url = get_well_known_url(self._config.issuer, external=True) - metadata_response = await self._http_client.get_json(url) - metadata.update(metadata_response) + try: + url = get_well_known_url(self._config.issuer, external=True) + metadata_response = await self._http_client.get_json(url) + metadata.update(metadata_response) + except Exception as e: + # This `Exception` bound is a bit broad, but at least expecting + # `twisted.internet.error.ConnectionRefusedError` + # and likely many other network or JSON errors. + raise OidcDiscoveryError() from e # override any discovered data with any settings in our config if self._config.authorization_endpoint: @@ -671,7 +724,12 @@ async def _load_metadata(self) -> OpenIDProviderMetadata: self._config.id_token_signing_alg_values_supported ) - self._validate_metadata(metadata) + try: + self._validate_metadata(metadata) + except ValueError as e: + # Wrap this error so that we can special-case it higher up + # Pass through `str(e)` as the message so tests can match it. + raise OidcMetadataError(str(e)) from e return metadata @@ -1685,7 +1743,7 @@ class JinjaOidcMappingProvider(OidcMappingProvider[JinjaOidcMappingConfig]): This is the default mapping provider. """ - def __init__(self, config: JinjaOidcMappingConfig, module_api: ModuleApi): + def __init__(self, config: JinjaOidcMappingConfig, module_api: "ModuleApi"): self._config = config @staticmethod diff --git a/synapse/handlers/sso.py b/synapse/handlers/sso.py index 0b2587f94fc..bb5ca329e00 100644 --- a/synapse/handlers/sso.py +++ b/synapse/handlers/sso.py @@ -70,6 +70,15 @@ class MappingException(Exception): """ +class SsoSetupError(Exception): + """ + Used to catch and tag errors relating to an SSO setup's. + + Used as the superclass of specific error classes, + as note we can't e.g. import the OIDC module unless OIDC dependencies are installed. + """ + + class SsoIdentityProvider(Protocol): """Abstract base class to be implemented by SSO Identity Providers diff --git a/synapse/res/templates/sso_error.html b/synapse/res/templates/sso_error.html index 6fa36c11c9d..e7e34c7293d 100644 --- a/synapse/res/templates/sso_error.html +++ b/synapse/res/templates/sso_error.html @@ -20,6 +20,27 @@

You are not allowed to log in here.

+{% elif error == "provider_unavailable" %} +
+

This login provider is unavailable right now.

+

+ There has been a problem which means that it's not currently possible + to log in with that provider. +

+

+ If your account has a password, or is connected to other login providers, + try those methods to log in. +

+

+ This issue could be temporary, so please try again later. + If the problem persists, please contact the server's administrator. +

+ + If you are the administrator of this server, please check the server logs for more information. + This could be caused by a server misconfiguration or an issue with the provider. + +
+ {% include "sso_footer.html" without context %} {% else %}

There was an error

diff --git a/synapse/rest/client/login.py b/synapse/rest/client/login.py index fe3cb9aa3df..a49b27d009f 100644 --- a/synapse/rest/client/login.py +++ b/synapse/rest/client/login.py @@ -18,7 +18,6 @@ # [This file includes modifications made by New Vector Limited] # # - import logging import re from typing import ( @@ -42,7 +41,7 @@ from synapse.api.ratelimiting import Ratelimiter from synapse.api.urls import CLIENT_API_PREFIX from synapse.appservice import ApplicationService -from synapse.handlers.sso import SsoIdentityProvider +from synapse.handlers.sso import SsoIdentityProvider, SsoSetupError from synapse.http import get_request_uri from synapse.http.server import HttpServer, finish_request from synapse.http.servlet import ( @@ -679,11 +678,26 @@ async def on_GET(self, request: SynapseRequest, idp_id: str | None = None) -> No args: dict[bytes, list[bytes]] = request.args # type: ignore client_redirect_url = parse_bytes_from_args(args, "redirectUrl", required=True) - sso_url = await self._sso_handler.handle_redirect_request( - request, - client_redirect_url, - idp_id, - ) + try: + sso_url = await self._sso_handler.handle_redirect_request( + request, + client_redirect_url, + idp_id, + ) + except SsoSetupError: + logger.exception( + "Login redirect failed because SSO/identity provider %r unavailable", + idp_id, + ) + # Show an error page that is slightly more friendly than JSON + self._sso_handler.render_error( + request, + "provider_unavailable", + "This login provider is currently unavailable on this homeserver.", + code=503, + ) + return + logger.info("Redirecting to %s", sso_url) request.redirect(sso_url) finish_request(request) diff --git a/tests/handlers/test_oidc.py b/tests/handlers/test_oidc.py index 4583afb625f..62b84c77a4d 100644 --- a/tests/handlers/test_oidc.py +++ b/tests/handlers/test_oidc.py @@ -19,7 +19,7 @@ # # import os -from typing import Any, Awaitable, ContextManager +from typing import Any, ContextManager from unittest.mock import ANY, AsyncMock, Mock, patch from urllib.parse import parse_qs, urlparse @@ -29,6 +29,7 @@ from synapse.handlers.sso import MappingException from synapse.http.site import SynapseRequest +from synapse.logging.context import make_deferred_yieldable from synapse.server import HomeServer from synapse.types import JsonDict, UserID from synapse.util.clock import Clock @@ -44,7 +45,7 @@ from authlib.oidc.core import UserInfo from authlib.oidc.discovery import OpenIDProviderMetadata - from synapse.handlers.oidc import Token, UserAttributeDict + from synapse.handlers.oidc import OidcMetadataError, Token, UserAttributeDict HAS_OIDC = True except ImportError: @@ -264,6 +265,29 @@ def test_discovery(self) -> None: self.get_success(self.provider.load_metadata()) self.fake_server.get_metadata_handler.assert_not_called() + @override_config({"oidc_config": {**DEFAULT_CONFIG, "discover": True}}) + def test_startup_metadata_preload_failure_is_logged(self) -> None: + """ + The metadata preload on startup causes logs to be emitted, but no + exceptions. + """ + with self.fake_server.buggy_endpoint(metadata=True): + with self.assertLogs("synapse.handlers.oidc", level="CRITICAL") as logs: + self.get_success( + make_deferred_yieldable(self.handler.preload_metadata()) + ) + + self.assertIn( + "CRITICAL:synapse.handlers.oidc:Error while preloading OIDC provider 'oidc'. Login may be broken!", + [line for record in logs.output for line in record.split("\n")], + ) + + # Even though the preload failed, if we try to load the metadata later, + # the attempt still goes through. + self.fake_server.get_metadata_handler.assert_not_called() + self.get_success(self.provider.load_metadata()) + self.fake_server.get_metadata_handler.assert_called_once() + @override_config({"oidc_config": {**EXPLICIT_ENDPOINT_CONFIG, "discover": True}}) def test_discovery_with_explicit_config(self) -> None: """ @@ -351,49 +375,53 @@ def test_validate_config(self) -> None: """Provider metadatas are extensively validated.""" h = self.provider - def force_load_metadata() -> Awaitable[None]: + def force_load_metadata() -> None: async def force_load() -> "OpenIDProviderMetadata": return await h.load_metadata(force=True) - return get_awaitable_result(force_load()) + get_awaitable_result(force_load()) # Default test config does not throw force_load_metadata() with self.metadata_edit({"issuer": None}): - self.assertRaisesRegex(ValueError, "issuer", force_load_metadata) + self.assertRaisesRegex(OidcMetadataError, "issuer", force_load_metadata) with self.metadata_edit({"issuer": "http://insecure/"}): - self.assertRaisesRegex(ValueError, "issuer", force_load_metadata) + self.assertRaisesRegex(OidcMetadataError, "issuer", force_load_metadata) with self.metadata_edit({"issuer": "https://invalid/?because=query"}): - self.assertRaisesRegex(ValueError, "issuer", force_load_metadata) + self.assertRaisesRegex(OidcMetadataError, "issuer", force_load_metadata) with self.metadata_edit({"authorization_endpoint": None}): self.assertRaisesRegex( - ValueError, "authorization_endpoint", force_load_metadata + OidcMetadataError, "authorization_endpoint", force_load_metadata ) with self.metadata_edit({"authorization_endpoint": "http://insecure/auth"}): self.assertRaisesRegex( - ValueError, "authorization_endpoint", force_load_metadata + OidcMetadataError, "authorization_endpoint", force_load_metadata ) with self.metadata_edit({"token_endpoint": None}): - self.assertRaisesRegex(ValueError, "token_endpoint", force_load_metadata) + self.assertRaisesRegex( + OidcMetadataError, "token_endpoint", force_load_metadata + ) with self.metadata_edit({"token_endpoint": "http://insecure/token"}): - self.assertRaisesRegex(ValueError, "token_endpoint", force_load_metadata) + self.assertRaisesRegex( + OidcMetadataError, "token_endpoint", force_load_metadata + ) with self.metadata_edit({"jwks_uri": None}): - self.assertRaisesRegex(ValueError, "jwks_uri", force_load_metadata) + self.assertRaisesRegex(OidcMetadataError, "jwks_uri", force_load_metadata) with self.metadata_edit({"jwks_uri": "http://insecure/jwks.json"}): - self.assertRaisesRegex(ValueError, "jwks_uri", force_load_metadata) + self.assertRaisesRegex(OidcMetadataError, "jwks_uri", force_load_metadata) with self.metadata_edit({"response_types_supported": ["id_token"]}): self.assertRaisesRegex( - ValueError, "response_types_supported", force_load_metadata + OidcMetadataError, "response_types_supported", force_load_metadata ) with self.metadata_edit( @@ -406,7 +434,7 @@ async def force_load() -> "OpenIDProviderMetadata": {"token_endpoint_auth_methods_supported": ["client_secret_post"]} ): self.assertRaisesRegex( - ValueError, + OidcMetadataError, "token_endpoint_auth_methods_supported", force_load_metadata, ) @@ -423,7 +451,9 @@ async def force_load() -> "OpenIDProviderMetadata": h._scopes = [] self.assertTrue(h._uses_userinfo) with self.metadata_edit({"userinfo_endpoint": None}): - self.assertRaisesRegex(ValueError, "userinfo_endpoint", force_load_metadata) + self.assertRaisesRegex( + OidcMetadataError, "userinfo_endpoint", force_load_metadata + ) with self.metadata_edit({"jwks_uri": None}): # Shouldn't raise with a valid userinfo, even without jwks diff --git a/tests/server.py b/tests/server.py index d17b2478e3b..55860701dae 100644 --- a/tests/server.py +++ b/tests/server.py @@ -1358,6 +1358,16 @@ def thread_pool() -> threadpool.ThreadPool: hs.get_media_sender_thread_pool = thread_pool # type: ignore[method-assign] + # Load the OIDC provider metadatas, if OIDC is enabled. + # This matches `start` in synapse/app/_base.py + # + # TODO: Extract common startup logic somewhere cleaner + if hs.config.oidc.oidc_enabled: + oidc = hs.get_oidc_handler() + # Preload the provider metadata. + # This will spawn fire-and-forget background processes. + oidc.preload_metadata() + # Load any configured modules into the homeserver module_api = hs.get_module_api() for module, module_config in hs.config.modules.loaded_modules: From 40d35a95e2ce56982f839f2d5f01bdad34e65453 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 25 Mar 2026 13:21:38 +0000 Subject: [PATCH 099/121] Bump tokio from 1.49.0 to 1.50.0 (#19596) --- Cargo.lock | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index c15688814ca..a004747af5b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1410,9 +1410,9 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "tokio" -version = "1.49.0" +version = "1.50.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72a2903cd7736441aac9df9d7688bd0ce48edccaadf181c3b90be801e81d3d86" +checksum = "27ad5e34374e03cfffefc301becb44e9dc3c17584f414349ebe29ed26661822d" dependencies = [ "bytes", "libc", From f2b325f86c7f45159b462aa295e6f69984d24748 Mon Sep 17 00:00:00 2001 From: Eric Eastwood Date: Wed, 25 Mar 2026 15:33:17 -0500 Subject: [PATCH 100/121] Demystify and deprecate `HomeserverTestCase.pump()` (Twisted reactor/clock) (#19602) Spawning from https://github.com/element-hq/synapse/pull/18416#discussion_r2967619735 --- changelog.d/19602.misc.1 | 1 + changelog.d/19602.misc.2 | 1 + tests/unittest.py | 37 ++++++++++++++++++++++++++++++++++++- 3 files changed, 38 insertions(+), 1 deletion(-) create mode 100644 changelog.d/19602.misc.1 create mode 100644 changelog.d/19602.misc.2 diff --git a/changelog.d/19602.misc.1 b/changelog.d/19602.misc.1 new file mode 100644 index 00000000000..222f26980db --- /dev/null +++ b/changelog.d/19602.misc.1 @@ -0,0 +1 @@ +Update `HomeserverTestCase.pump()` docstring to demystify behavior (Twisted reactor/clock). diff --git a/changelog.d/19602.misc.2 b/changelog.d/19602.misc.2 new file mode 100644 index 00000000000..4cadad653cf --- /dev/null +++ b/changelog.d/19602.misc.2 @@ -0,0 +1 @@ +Deprecate `HomeserverTestCase.pump()` in favor of more direct `HomeserverTestCase.reactor.advance(...)` usage. diff --git a/tests/unittest.py b/tests/unittest.py index 6022c750d03..03db9a42827 100644 --- a/tests/unittest.py +++ b/tests/unittest.py @@ -697,8 +697,43 @@ async def run_bg_updates() -> None: def pump(self, by: float = 0.0) -> None: """ - Pump the reactor enough that Deferreds will fire. + XXX: Deprecated: This method is deprecated. Use `self.reactor.advance(...)` + directly instead. + + Pump the reactor enough that `clock.call_later` scheduled callbacks will fire. + + To demystify this function, it simply advances time by the number of seconds + specified (defaults to `0`, we also multiply by 100, so `pump(1)` is 100 seconds + in 1 second steps/increments) whilst calling any pending callbacks, allowing any + queued/pending tasks to run because enough time has passed. + + So for example, if you have some Synapse code that does + `clock.call_later(Duration(seconds=2), callback)`, then calling + `self.pump(by=0.02)` will advance time by 2 seconds, which is enough for that + callback to be ready to run now. Same for `clock.sleep(...)` , + `clock.looping_call(...)`, and whatever other clock utilities that use + `clock.call_later` under the hood for scheduling tasks. Trying to use + `pump(by=...)` with exact math to meet a specific deadline feels pretty dirty + though which is why we recommend using `self.reactor.advance(...)` directly + nowadays. + + We don't have any exact historical context for why `pump()` was introduced into + the codebase beyond the code itself. We assume that we multiply by 100 so that + when you use the clock to schedule something that schedules more things, it + tries to run the whole chain to completion. + + XXX: If you're having to call this function, please call out in comments, which + scheduled thing you're aiming to trigger. Please also check whether the + `pump(...)` is even necessary as it was often misused. + + Args: + by: The time increment in seconds to advance time by. We will advance time + in 100 steps, each step by this value. """ + # We multiply by 100, so `pump(1)` actually advances time by 100 seconds in 1 + # second steps/increments. We assume this was done so that when you use the + # clock to schedule something that schedules more things, it tries to run the + # whole chain to completion. self.reactor.pump([by] * 100) def get_success(self, d: Awaitable[TV], by: float = 0.0) -> TV: From f545aa4f33377f2c68aad7032afbe5a395c35cbc Mon Sep 17 00:00:00 2001 From: Erik Johnston Date: Thu, 26 Mar 2026 09:17:31 +0000 Subject: [PATCH 101/121] Port `RoomVersion` to Rust (#19589) Principally so that we can share the same room version configuration between Python and Rust. For the most part, this is a direct port. Some special handling has had to go into `KNOWN_ROOM_VERSIONS` so that it can be sensibly shared between Python and Rust, since we do update it during config parsing. --------- Co-authored-by: Andrew Morgan <1342360+anoadragon453@users.noreply.github.com> Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- changelog.d/19589.misc | 1 + rust/src/lib.rs | 2 + rust/src/room_versions.rs | 825 +++++++++++++++++++++++++ synapse/api/room_versions.py | 496 +-------------- synapse/config/experimental.py | 3 +- synapse/event_auth.py | 24 +- synapse/synapse_rust/push.pyi | 2 +- synapse/synapse_rust/room_versions.pyi | 142 +++++ tests/events/test_utils.py | 12 +- 9 files changed, 995 insertions(+), 512 deletions(-) create mode 100644 changelog.d/19589.misc create mode 100644 rust/src/room_versions.rs create mode 100644 synapse/synapse_rust/room_versions.pyi diff --git a/changelog.d/19589.misc b/changelog.d/19589.misc new file mode 100644 index 00000000000..f3c15326c67 --- /dev/null +++ b/changelog.d/19589.misc @@ -0,0 +1 @@ +Port `RoomVersion` to Rust. diff --git a/rust/src/lib.rs b/rust/src/lib.rs index 83b8de7b64a..a6e01fce641 100644 --- a/rust/src/lib.rs +++ b/rust/src/lib.rs @@ -15,6 +15,7 @@ pub mod matrix_const; pub mod msc4388_rendezvous; pub mod push; pub mod rendezvous; +pub mod room_versions; pub mod segmenter; lazy_static! { @@ -58,6 +59,7 @@ fn synapse_rust(py: Python<'_>, m: &Bound<'_, PyModule>) -> PyResult<()> { rendezvous::register_module(py, m)?; msc4388_rendezvous::register_module(py, m)?; segmenter::register_module(py, m)?; + room_versions::register_module(py, m)?; Ok(()) } diff --git a/rust/src/room_versions.rs b/rust/src/room_versions.rs new file mode 100644 index 00000000000..c0b304e18c9 --- /dev/null +++ b/rust/src/room_versions.rs @@ -0,0 +1,825 @@ +/* + * This file is licensed under the Affero General Public License (AGPL) version 3. + * + * Copyright (C) 2026 Element Creations Ltd. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * See the GNU Affero General Public License for more details: + * . + */ + +//! Rust implementation of room version definitions. + +use std::sync::{Arc, LazyLock, RwLock}; + +use pyo3::{ + exceptions::{PyKeyError, PyRuntimeError}, + prelude::*, + types::{PyFrozenSet, PyIterator, PyModule, PyModuleMethods}, + Bound, IntoPyObjectExt, PyResult, Python, +}; + +/// Internal enum for tracking the version of the event format, +/// independently of the room version. +/// +/// To reduce confusion, the event format versions are named after the room +/// versions that they were used or introduced in. +/// The concept of an 'event format version' is specific to Synapse (the +/// specification does not mention this term.) +#[pyclass(frozen)] +pub struct EventFormatVersions {} + +#[pymethods] +impl EventFormatVersions { + /// $id:server event id format: used for room v1 and v2 + #[classattr] + const ROOM_V1_V2: i32 = 1; + /// MSC1659-style $hash event id format: used for room v3 + #[classattr] + const ROOM_V3: i32 = 2; + /// MSC1884-style $hash format: introduced for room v4 + #[classattr] + const ROOM_V4_PLUS: i32 = 3; + /// MSC4291 room IDs as hashes: introduced for room HydraV11 + #[classattr] + const ROOM_V11_HYDRA_PLUS: i32 = 4; +} + +/// Enum to identify the state resolution algorithms. +#[pyclass(frozen)] +pub struct StateResolutionVersions {} + +#[pymethods] +impl StateResolutionVersions { + /// Room v1 state res + #[classattr] + const V1: i32 = 1; + /// MSC1442 state res: room v2 and later + #[classattr] + const V2: i32 = 2; + /// MSC4297 state res + #[classattr] + const V2_1: i32 = 3; +} + +/// Room disposition constants. +#[pyclass(frozen)] +pub struct RoomDisposition {} + +#[pymethods] +impl RoomDisposition { + #[classattr] + const STABLE: &'static str = "stable"; + #[classattr] + const UNSTABLE: &'static str = "unstable"; +} + +/// Enum for listing possible MSC3931 room version feature flags, for push rules. +#[pyclass(frozen)] +pub struct PushRuleRoomFlag {} + +#[pymethods] +impl PushRuleRoomFlag { + /// MSC3932: Room version supports MSC1767 Extensible Events. + #[classattr] + const EXTENSIBLE_EVENTS: &'static str = "org.matrix.msc3932.extensible_events"; +} + +/// An object which describes the unique attributes of a room version. +#[pyclass(frozen, eq, hash, get_all)] +#[derive(Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)] +pub struct RoomVersion { + /// The identifier for this version. + pub identifier: &'static str, + /// One of the RoomDisposition constants. + pub disposition: &'static str, + /// One of the EventFormatVersions constants. + pub event_format: i32, + /// One of the StateResolutionVersions constants. + pub state_res: i32, + pub enforce_key_validity: bool, + /// Before MSC2432, m.room.aliases had special auth rules and redaction rules. + pub special_case_aliases_auth: bool, + /// Strictly enforce canonicaljson, do not allow: + /// * Integers outside the range of [-2^53 + 1, 2^53 - 1] + /// * Floats + /// * NaN, Infinity, -Infinity + pub strict_canonicaljson: bool, + /// MSC2209: Check 'notifications' key while verifying + /// m.room.power_levels auth rules. + pub limit_notifications_power_levels: bool, + /// MSC3820: No longer include the creator in m.room.create events (room version 11). + pub implicit_room_creator: bool, + /// MSC3820: Apply updated redaction rules algorithm from room version 11. + pub updated_redaction_rules: bool, + /// Support the 'restricted' join rule. + pub restricted_join_rule: bool, + /// Support for the proper redaction rules for the restricted join rule. + /// This requires restricted_join_rule to be enabled. + pub restricted_join_rule_fix: bool, + /// Support the 'knock' join rule. + pub knock_join_rule: bool, + /// MSC3389: Protect relation information from redaction. + pub msc3389_relation_redactions: bool, + /// Support the 'knock_restricted' join rule. + pub knock_restricted_join_rule: bool, + /// Enforce integer power levels. + pub enforce_int_power_levels: bool, + /// MSC3931: Adds a push rule condition for "room version feature flags", making + /// some push rules room version dependent. Note that adding a flag to this list + /// is not enough to mark it "supported": the push rule evaluator also needs to + /// support the flag. Unknown flags are ignored by the evaluator, making conditions + /// fail if used. Values from PushRuleRoomFlag. + pub msc3931_push_features: &'static [&'static str], + /// MSC3757: Restricting who can overwrite a state event. + pub msc3757_enabled: bool, + /// MSC4289: Creator power enabled. + pub msc4289_creator_power_enabled: bool, + /// MSC4291: Room IDs as hashes of the create event. + pub msc4291_room_ids_as_hashes: bool, + /// Set of room versions where Synapse strictly enforces event key size limits + /// in bytes, rather than in codepoints. + /// + /// In these room versions, we are stricter with event size validation. + pub strict_event_byte_limits_room_versions: bool, +} + +const ROOM_VERSION_V1: RoomVersion = RoomVersion { + identifier: "1", + disposition: RoomDisposition::STABLE, + event_format: EventFormatVersions::ROOM_V1_V2, + state_res: StateResolutionVersions::V1, + enforce_key_validity: false, + special_case_aliases_auth: true, + strict_canonicaljson: false, + limit_notifications_power_levels: false, + implicit_room_creator: false, + updated_redaction_rules: false, + restricted_join_rule: false, + restricted_join_rule_fix: false, + knock_join_rule: false, + msc3389_relation_redactions: false, + knock_restricted_join_rule: false, + enforce_int_power_levels: false, + msc3931_push_features: &[], + msc3757_enabled: false, + msc4289_creator_power_enabled: false, + msc4291_room_ids_as_hashes: false, + strict_event_byte_limits_room_versions: false, +}; + +const ROOM_VERSION_V2: RoomVersion = RoomVersion { + identifier: "2", + disposition: RoomDisposition::STABLE, + event_format: EventFormatVersions::ROOM_V1_V2, + state_res: StateResolutionVersions::V2, + enforce_key_validity: false, + special_case_aliases_auth: true, + strict_canonicaljson: false, + limit_notifications_power_levels: false, + implicit_room_creator: false, + updated_redaction_rules: false, + restricted_join_rule: false, + restricted_join_rule_fix: false, + knock_join_rule: false, + msc3389_relation_redactions: false, + knock_restricted_join_rule: false, + enforce_int_power_levels: false, + msc3931_push_features: &[], + msc3757_enabled: false, + msc4289_creator_power_enabled: false, + msc4291_room_ids_as_hashes: false, + strict_event_byte_limits_room_versions: false, +}; + +const ROOM_VERSION_V3: RoomVersion = RoomVersion { + identifier: "3", + disposition: RoomDisposition::STABLE, + event_format: EventFormatVersions::ROOM_V3, + state_res: StateResolutionVersions::V2, + enforce_key_validity: false, + special_case_aliases_auth: true, + strict_canonicaljson: false, + limit_notifications_power_levels: false, + implicit_room_creator: false, + updated_redaction_rules: false, + restricted_join_rule: false, + restricted_join_rule_fix: false, + knock_join_rule: false, + msc3389_relation_redactions: false, + knock_restricted_join_rule: false, + enforce_int_power_levels: false, + msc3931_push_features: &[], + msc3757_enabled: false, + msc4289_creator_power_enabled: false, + msc4291_room_ids_as_hashes: false, + strict_event_byte_limits_room_versions: false, +}; + +const ROOM_VERSION_V4: RoomVersion = RoomVersion { + identifier: "4", + disposition: RoomDisposition::STABLE, + event_format: EventFormatVersions::ROOM_V4_PLUS, + state_res: StateResolutionVersions::V2, + enforce_key_validity: false, + special_case_aliases_auth: true, + strict_canonicaljson: false, + limit_notifications_power_levels: false, + implicit_room_creator: false, + updated_redaction_rules: false, + restricted_join_rule: false, + restricted_join_rule_fix: false, + knock_join_rule: false, + msc3389_relation_redactions: false, + knock_restricted_join_rule: false, + enforce_int_power_levels: false, + msc3931_push_features: &[], + msc3757_enabled: false, + msc4289_creator_power_enabled: false, + msc4291_room_ids_as_hashes: false, + strict_event_byte_limits_room_versions: false, +}; + +const ROOM_VERSION_V5: RoomVersion = RoomVersion { + identifier: "5", + disposition: RoomDisposition::STABLE, + event_format: EventFormatVersions::ROOM_V4_PLUS, + state_res: StateResolutionVersions::V2, + enforce_key_validity: true, + special_case_aliases_auth: true, + strict_canonicaljson: false, + limit_notifications_power_levels: false, + implicit_room_creator: false, + updated_redaction_rules: false, + restricted_join_rule: false, + restricted_join_rule_fix: false, + knock_join_rule: false, + msc3389_relation_redactions: false, + knock_restricted_join_rule: false, + enforce_int_power_levels: false, + msc3931_push_features: &[], + msc3757_enabled: false, + msc4289_creator_power_enabled: false, + msc4291_room_ids_as_hashes: false, + strict_event_byte_limits_room_versions: false, +}; + +const ROOM_VERSION_V6: RoomVersion = RoomVersion { + identifier: "6", + disposition: RoomDisposition::STABLE, + event_format: EventFormatVersions::ROOM_V4_PLUS, + state_res: StateResolutionVersions::V2, + enforce_key_validity: true, + special_case_aliases_auth: false, + strict_canonicaljson: true, + limit_notifications_power_levels: true, + implicit_room_creator: false, + updated_redaction_rules: false, + restricted_join_rule: false, + restricted_join_rule_fix: false, + knock_join_rule: false, + msc3389_relation_redactions: false, + knock_restricted_join_rule: false, + enforce_int_power_levels: false, + msc3931_push_features: &[], + msc3757_enabled: false, + msc4289_creator_power_enabled: false, + msc4291_room_ids_as_hashes: false, + strict_event_byte_limits_room_versions: false, +}; + +const ROOM_VERSION_V7: RoomVersion = RoomVersion { + identifier: "7", + disposition: RoomDisposition::STABLE, + event_format: EventFormatVersions::ROOM_V4_PLUS, + state_res: StateResolutionVersions::V2, + enforce_key_validity: true, + special_case_aliases_auth: false, + strict_canonicaljson: true, + limit_notifications_power_levels: true, + implicit_room_creator: false, + updated_redaction_rules: false, + restricted_join_rule: false, + restricted_join_rule_fix: false, + knock_join_rule: true, + msc3389_relation_redactions: false, + knock_restricted_join_rule: false, + enforce_int_power_levels: false, + msc3931_push_features: &[], + msc3757_enabled: false, + msc4289_creator_power_enabled: false, + msc4291_room_ids_as_hashes: false, + strict_event_byte_limits_room_versions: false, +}; + +const ROOM_VERSION_V8: RoomVersion = RoomVersion { + identifier: "8", + disposition: RoomDisposition::STABLE, + event_format: EventFormatVersions::ROOM_V4_PLUS, + state_res: StateResolutionVersions::V2, + enforce_key_validity: true, + special_case_aliases_auth: false, + strict_canonicaljson: true, + limit_notifications_power_levels: true, + implicit_room_creator: false, + updated_redaction_rules: false, + restricted_join_rule: true, + restricted_join_rule_fix: false, + knock_join_rule: true, + msc3389_relation_redactions: false, + knock_restricted_join_rule: false, + enforce_int_power_levels: false, + msc3931_push_features: &[], + msc3757_enabled: false, + msc4289_creator_power_enabled: false, + msc4291_room_ids_as_hashes: false, + strict_event_byte_limits_room_versions: false, +}; + +const ROOM_VERSION_V9: RoomVersion = RoomVersion { + identifier: "9", + disposition: RoomDisposition::STABLE, + event_format: EventFormatVersions::ROOM_V4_PLUS, + state_res: StateResolutionVersions::V2, + enforce_key_validity: true, + special_case_aliases_auth: false, + strict_canonicaljson: true, + limit_notifications_power_levels: true, + implicit_room_creator: false, + updated_redaction_rules: false, + restricted_join_rule: true, + restricted_join_rule_fix: true, + knock_join_rule: true, + msc3389_relation_redactions: false, + knock_restricted_join_rule: false, + enforce_int_power_levels: false, + msc3931_push_features: &[], + msc3757_enabled: false, + msc4289_creator_power_enabled: false, + msc4291_room_ids_as_hashes: false, + strict_event_byte_limits_room_versions: false, +}; + +const ROOM_VERSION_V10: RoomVersion = RoomVersion { + identifier: "10", + disposition: RoomDisposition::STABLE, + event_format: EventFormatVersions::ROOM_V4_PLUS, + state_res: StateResolutionVersions::V2, + enforce_key_validity: true, + special_case_aliases_auth: false, + strict_canonicaljson: true, + limit_notifications_power_levels: true, + implicit_room_creator: false, + updated_redaction_rules: false, + restricted_join_rule: true, + restricted_join_rule_fix: true, + knock_join_rule: true, + msc3389_relation_redactions: false, + knock_restricted_join_rule: true, + enforce_int_power_levels: true, + msc3931_push_features: &[], + msc3757_enabled: false, + msc4289_creator_power_enabled: false, + msc4291_room_ids_as_hashes: false, + strict_event_byte_limits_room_versions: false, +}; + +/// MSC3389 (Redaction changes for events with a relation) based on room version "10". +const ROOM_VERSION_MSC3389V10: RoomVersion = RoomVersion { + identifier: "org.matrix.msc3389.10", + disposition: RoomDisposition::UNSTABLE, + event_format: EventFormatVersions::ROOM_V4_PLUS, + state_res: StateResolutionVersions::V2, + enforce_key_validity: true, + special_case_aliases_auth: false, + strict_canonicaljson: true, + limit_notifications_power_levels: true, + implicit_room_creator: false, + updated_redaction_rules: false, + restricted_join_rule: true, + restricted_join_rule_fix: true, + knock_join_rule: true, + msc3389_relation_redactions: true, // Changed from v10 + knock_restricted_join_rule: true, + enforce_int_power_levels: true, + msc3931_push_features: &[], + msc3757_enabled: false, + msc4289_creator_power_enabled: false, + msc4291_room_ids_as_hashes: false, + strict_event_byte_limits_room_versions: true, +}; + +/// MSC1767 (Extensible Events) based on room version "10". +const ROOM_VERSION_MSC1767V10: RoomVersion = RoomVersion { + identifier: "org.matrix.msc1767.10", + disposition: RoomDisposition::UNSTABLE, + event_format: EventFormatVersions::ROOM_V4_PLUS, + state_res: StateResolutionVersions::V2, + enforce_key_validity: true, + special_case_aliases_auth: false, + strict_canonicaljson: true, + limit_notifications_power_levels: true, + implicit_room_creator: false, + updated_redaction_rules: false, + restricted_join_rule: true, + restricted_join_rule_fix: true, + knock_join_rule: true, + msc3389_relation_redactions: false, + knock_restricted_join_rule: true, + enforce_int_power_levels: true, + msc3931_push_features: &[PushRuleRoomFlag::EXTENSIBLE_EVENTS], + msc3757_enabled: false, + msc4289_creator_power_enabled: false, + msc4291_room_ids_as_hashes: false, + strict_event_byte_limits_room_versions: false, +}; + +/// MSC3757 (Restricting who can overwrite a state event) based on room version "10". +const ROOM_VERSION_MSC3757V10: RoomVersion = RoomVersion { + identifier: "org.matrix.msc3757.10", + disposition: RoomDisposition::UNSTABLE, + event_format: EventFormatVersions::ROOM_V4_PLUS, + state_res: StateResolutionVersions::V2, + enforce_key_validity: true, + special_case_aliases_auth: false, + strict_canonicaljson: true, + limit_notifications_power_levels: true, + implicit_room_creator: false, + updated_redaction_rules: false, + restricted_join_rule: true, + restricted_join_rule_fix: true, + knock_join_rule: true, + msc3389_relation_redactions: false, + knock_restricted_join_rule: true, + enforce_int_power_levels: true, + msc3931_push_features: &[], + msc3757_enabled: true, + msc4289_creator_power_enabled: false, + msc4291_room_ids_as_hashes: false, + strict_event_byte_limits_room_versions: false, +}; + +const ROOM_VERSION_V11: RoomVersion = RoomVersion { + identifier: "11", + disposition: RoomDisposition::STABLE, + event_format: EventFormatVersions::ROOM_V4_PLUS, + state_res: StateResolutionVersions::V2, + enforce_key_validity: true, + special_case_aliases_auth: false, + strict_canonicaljson: true, + limit_notifications_power_levels: true, + implicit_room_creator: true, // Used by MSC3820 + updated_redaction_rules: true, // Used by MSC3820 + restricted_join_rule: true, + restricted_join_rule_fix: true, + knock_join_rule: true, + msc3389_relation_redactions: false, + knock_restricted_join_rule: true, + enforce_int_power_levels: true, + msc3931_push_features: &[], + msc3757_enabled: false, + msc4289_creator_power_enabled: false, + msc4291_room_ids_as_hashes: false, + strict_event_byte_limits_room_versions: true, // Changed from v10 +}; + +/// MSC3757 (Restricting who can overwrite a state event) based on room version "11". +const ROOM_VERSION_MSC3757V11: RoomVersion = RoomVersion { + identifier: "org.matrix.msc3757.11", + disposition: RoomDisposition::UNSTABLE, + event_format: EventFormatVersions::ROOM_V4_PLUS, + state_res: StateResolutionVersions::V2, + enforce_key_validity: true, + special_case_aliases_auth: false, + strict_canonicaljson: true, + limit_notifications_power_levels: true, + implicit_room_creator: true, // Used by MSC3820 + updated_redaction_rules: true, // Used by MSC3820 + restricted_join_rule: true, + restricted_join_rule_fix: true, + knock_join_rule: true, + msc3389_relation_redactions: false, + knock_restricted_join_rule: true, + enforce_int_power_levels: true, + msc3931_push_features: &[], + msc3757_enabled: true, + msc4289_creator_power_enabled: false, + msc4291_room_ids_as_hashes: false, + strict_event_byte_limits_room_versions: true, +}; + +const ROOM_VERSION_HYDRA_V11: RoomVersion = RoomVersion { + identifier: "org.matrix.hydra.11", + disposition: RoomDisposition::UNSTABLE, + event_format: EventFormatVersions::ROOM_V11_HYDRA_PLUS, + state_res: StateResolutionVersions::V2_1, // Changed from v11 + enforce_key_validity: true, + special_case_aliases_auth: false, + strict_canonicaljson: true, + limit_notifications_power_levels: true, + implicit_room_creator: true, // Used by MSC3820 + updated_redaction_rules: true, // Used by MSC3820 + restricted_join_rule: true, + restricted_join_rule_fix: true, + knock_join_rule: true, + msc3389_relation_redactions: false, + knock_restricted_join_rule: true, + enforce_int_power_levels: true, + msc3931_push_features: &[], + msc3757_enabled: false, + msc4289_creator_power_enabled: true, // Changed from v11 + msc4291_room_ids_as_hashes: true, // Changed from v11 + strict_event_byte_limits_room_versions: true, +}; + +const ROOM_VERSION_V12: RoomVersion = RoomVersion { + identifier: "12", + disposition: RoomDisposition::STABLE, + event_format: EventFormatVersions::ROOM_V11_HYDRA_PLUS, + state_res: StateResolutionVersions::V2_1, // Changed from v11 + enforce_key_validity: true, + special_case_aliases_auth: false, + strict_canonicaljson: true, + limit_notifications_power_levels: true, + implicit_room_creator: true, // Used by MSC3820 + updated_redaction_rules: true, // Used by MSC3820 + restricted_join_rule: true, + restricted_join_rule_fix: true, + knock_join_rule: true, + msc3389_relation_redactions: false, + knock_restricted_join_rule: true, + enforce_int_power_levels: true, + msc3931_push_features: &[], + msc3757_enabled: false, + msc4289_creator_power_enabled: true, // Changed from v11 + msc4291_room_ids_as_hashes: true, // Changed from v11 + strict_event_byte_limits_room_versions: true, +}; + +/// Helper class for managing the known room versions, and providing dict-like +/// access to them for Python. +/// +/// Note: this is not necessarily all room versions, as we may not want to +/// support all experimental room versions. +/// +/// Note: room versions can be added to this mapping at startup (allowing +/// support for experimental room versions to be behind experimental feature +/// flags). +#[pyclass(frozen, mapping)] +#[derive(Clone)] +pub struct KnownRoomVersionsMapping { + // Note we use a Vec here to ensure that the order of keys is + // deterministic. We rely on this when generating parameterized tests in + // Python, as Python will generate the tests names including the room + // version and index (and we need test names to be stable to e.g. support + // running tests in parallel). + pub versions: Arc>>, +} + +#[pymethods] +impl KnownRoomVersionsMapping { + /// Add a new room version to the mapping, indicating that this instance + /// supports it. + fn add_room_version(&self, version: RoomVersion) -> PyResult<()> { + let mut versions = self + .versions + .write() + .map_err(|_| PyRuntimeError::new_err("KnownRoomVersionsMapping lock poisoned"))?; + + if versions.iter().any(|v| v.identifier == version.identifier) { + // We already have this room version, so we don't add it again (as + // otherwise we'd end up with duplicates). + return Ok(()); + } + + versions.push(version); + Ok(()) + } + + fn __getitem__(&self, key: &str) -> PyResult { + let versions = self.versions.read().unwrap(); + versions + .iter() + .find(|v| v.identifier == key) + .copied() + .ok_or_else(|| PyKeyError::new_err(key.to_string())) + } + + fn __contains__(&self, key: &str) -> PyResult { + let versions = self.versions.read().unwrap(); + Ok(versions.iter().any(|v| v.identifier == key)) + } + + fn keys(&self) -> PyResult> { + // Note that technically we should return a view here (that also acts + // like a Set *and* has a stable ordering). We don't depend on this, so + // for simplicity we just return a list of the keys. + let versions = self.versions.read().unwrap(); + Ok(versions.iter().map(|v| v.identifier).collect()) + } + + fn values(&self) -> PyResult> { + let versions = self.versions.read().unwrap(); + Ok(versions.clone()) + } + + fn items(&self) -> PyResult> { + // Note that technically we should return a view here (that also acts + // like a Set *and* has a stable ordering). We don't depend on this, so + // for simplicity we just return a list of the items. + let versions = self.versions.read().unwrap(); + Ok(versions.iter().map(|v| (v.identifier, *v)).collect()) + } + + #[pyo3(signature = (key, default=None))] + fn get<'py>( + &self, + py: Python<'py>, + key: Bound<'py, PyAny>, + default: Option>, + ) -> PyResult>> { + // We need to accept anything as the key, but we know that only strings + // are valid keys, so if it's not a string we just return the default. + let Ok(key) = key.extract::<&str>() else { + return Ok(default); + }; + + let versions = self.versions.read().unwrap(); + if let Some(version) = versions.iter().find(|v| v.identifier == key).copied() { + return Ok(Some(version.into_bound_py_any(py)?)); + } + + Ok(default) + } + + fn __len__(&self) -> PyResult { + let versions = self + .versions + .read() + .map_err(|_| PyRuntimeError::new_err("KnownRoomVersionsMapping lock poisoned"))?; + Ok(versions.len()) + } + + fn __iter__<'py>(&self, py: Python<'py>) -> PyResult> { + let key_list = self.keys()?; + + let bound_key_list = key_list.into_bound_py_any(py)?; + PyIterator::from_object(&bound_key_list) + } +} + +impl<'py> IntoPyObject<'py> for &KnownRoomVersionsMapping { + type Target = KnownRoomVersionsMapping; + + type Output = Bound<'py, Self::Target>; + + type Error = PyErr; + + fn into_pyobject(self, py: Python<'py>) -> Result { + self.clone().into_pyobject(py) + } +} + +/// All room versions this instance knows about, used to build the +/// `KNOWN_ROOM_VERSIONS` dict. +/// +/// Note: this is not necessarily all room versions, as we may not want to +/// support all experimental room versions. +static KNOWN_ROOM_VERSIONS: LazyLock = LazyLock::new(|| { + let vec = vec![ + ROOM_VERSION_V1, + ROOM_VERSION_V2, + ROOM_VERSION_V3, + ROOM_VERSION_V4, + ROOM_VERSION_V5, + ROOM_VERSION_V6, + ROOM_VERSION_V7, + ROOM_VERSION_V8, + ROOM_VERSION_V9, + ROOM_VERSION_V10, + ROOM_VERSION_V11, + ROOM_VERSION_V12, + ROOM_VERSION_MSC3757V10, + ROOM_VERSION_MSC3757V11, + ROOM_VERSION_HYDRA_V11, + ]; + + KnownRoomVersionsMapping { + versions: Arc::new(RwLock::new(vec)), + } +}); + +/// Container class for room version constants. +/// +/// This should contain all room versions that we know about. +#[pyclass(frozen)] +pub struct RoomVersions {} + +#[pymethods] +#[allow(non_snake_case)] +impl RoomVersions { + #[classattr] + fn V1(py: Python<'_>) -> PyResult> { + ROOM_VERSION_V1.into_py_any(py) + } + #[classattr] + fn V2(py: Python<'_>) -> PyResult> { + ROOM_VERSION_V2.into_py_any(py) + } + #[classattr] + fn V3(py: Python<'_>) -> PyResult> { + ROOM_VERSION_V3.into_py_any(py) + } + #[classattr] + fn V4(py: Python<'_>) -> PyResult> { + ROOM_VERSION_V4.into_py_any(py) + } + #[classattr] + fn V5(py: Python<'_>) -> PyResult> { + ROOM_VERSION_V5.into_py_any(py) + } + #[classattr] + fn V6(py: Python<'_>) -> PyResult> { + ROOM_VERSION_V6.into_py_any(py) + } + #[classattr] + fn V7(py: Python<'_>) -> PyResult> { + ROOM_VERSION_V7.into_py_any(py) + } + #[classattr] + fn V8(py: Python<'_>) -> PyResult> { + ROOM_VERSION_V8.into_py_any(py) + } + #[classattr] + fn V9(py: Python<'_>) -> PyResult> { + ROOM_VERSION_V9.into_py_any(py) + } + #[classattr] + fn V10(py: Python<'_>) -> PyResult> { + ROOM_VERSION_V10.into_py_any(py) + } + #[classattr] + fn MSC1767v10(py: Python<'_>) -> PyResult> { + ROOM_VERSION_MSC1767V10.into_py_any(py) + } + #[classattr] + fn MSC3389v10(py: Python<'_>) -> PyResult> { + ROOM_VERSION_MSC3389V10.into_py_any(py) + } + #[classattr] + fn MSC3757v10(py: Python<'_>) -> PyResult> { + ROOM_VERSION_MSC3757V10.into_py_any(py) + } + #[classattr] + fn V11(py: Python<'_>) -> PyResult> { + ROOM_VERSION_V11.into_py_any(py) + } + #[classattr] + fn MSC3757v11(py: Python<'_>) -> PyResult> { + ROOM_VERSION_MSC3757V11.into_py_any(py) + } + #[classattr] + fn HydraV11(py: Python<'_>) -> PyResult> { + ROOM_VERSION_HYDRA_V11.into_py_any(py) + } + #[classattr] + fn V12(py: Python<'_>) -> PyResult> { + ROOM_VERSION_V12.into_py_any(py) + } +} + +/// Called when registering modules with python. +pub fn register_module(py: Python<'_>, m: &Bound<'_, PyModule>) -> PyResult<()> { + let child_module = PyModule::new(py, "room_versions")?; + child_module.add_class::()?; + child_module.add_class::()?; + child_module.add_class::()?; + child_module.add_class::()?; + child_module.add_class::()?; + child_module.add_class::()?; + child_module.add_class::()?; + + // Build KNOWN_EVENT_FORMAT_VERSIONS as a frozenset + let known_ef: [i32; 4] = [ + EventFormatVersions::ROOM_V1_V2, + EventFormatVersions::ROOM_V3, + EventFormatVersions::ROOM_V4_PLUS, + EventFormatVersions::ROOM_V11_HYDRA_PLUS, + ]; + let known_event_format_versions = PyFrozenSet::new(py, known_ef)?; + child_module.add("KNOWN_EVENT_FORMAT_VERSIONS", known_event_format_versions)?; + child_module.add("KNOWN_ROOM_VERSIONS", &*KNOWN_ROOM_VERSIONS)?; + + m.add_submodule(&child_module)?; + + // Register in sys.modules + py.import("sys")? + .getattr("modules")? + .set_item("synapse.synapse_rust.room_versions", child_module)?; + + Ok(()) +} diff --git a/synapse/api/room_versions.py b/synapse/api/room_versions.py index 13c7758638d..0035d7a58bb 100644 --- a/synapse/api/room_versions.py +++ b/synapse/api/room_versions.py @@ -18,480 +18,22 @@ # # - -import attr - - -class EventFormatVersions: - """This is an internal enum for tracking the version of the event format, - independently of the room version. - - To reduce confusion, the event format versions are named after the room - versions that they were used or introduced in. - The concept of an 'event format version' is specific to Synapse (the - specification does not mention this term.) - """ - - ROOM_V1_V2 = 1 # $id:server event id format: used for room v1 and v2 - ROOM_V3 = 2 # MSC1659-style $hash event id format: used for room v3 - ROOM_V4_PLUS = 3 # MSC1884-style $hash format: introduced for room v4 - ROOM_V11_HYDRA_PLUS = 4 # MSC4291 room IDs as hashes: introduced for room HydraV11 - - -KNOWN_EVENT_FORMAT_VERSIONS = { - EventFormatVersions.ROOM_V1_V2, - EventFormatVersions.ROOM_V3, - EventFormatVersions.ROOM_V4_PLUS, - EventFormatVersions.ROOM_V11_HYDRA_PLUS, -} - - -class StateResolutionVersions: - """Enum to identify the state resolution algorithms""" - - V1 = 1 # room v1 state res - V2 = 2 # MSC1442 state res: room v2 and later - V2_1 = 3 # MSC4297 state res - - -class RoomDisposition: - STABLE = "stable" - UNSTABLE = "unstable" - - -class PushRuleRoomFlag: - """Enum for listing possible MSC3931 room version feature flags, for push rules""" - - # MSC3932: Room version supports MSC1767 Extensible Events. - EXTENSIBLE_EVENTS = "org.matrix.msc3932.extensible_events" - - -@attr.s(slots=True, frozen=True, auto_attribs=True) -class RoomVersion: - """An object which describes the unique attributes of a room version.""" - - identifier: str # the identifier for this version - disposition: str # one of the RoomDispositions - event_format: int # one of the EventFormatVersions - state_res: int # one of the StateResolutionVersions - enforce_key_validity: bool - - # Before MSC2432, m.room.aliases had special auth rules and redaction rules - special_case_aliases_auth: bool - # Strictly enforce canonicaljson, do not allow: - # * Integers outside the range of [-2 ^ 53 + 1, 2 ^ 53 - 1] - # * Floats - # * NaN, Infinity, -Infinity - strict_canonicaljson: bool - # MSC2209: Check 'notifications' key while verifying - # m.room.power_levels auth rules. - limit_notifications_power_levels: bool - # MSC3820: No longer include the creator in m.room.create events (room version 11) - implicit_room_creator: bool - # MSC3820: Apply updated redaction rules algorithm from room version 11 - updated_redaction_rules: bool - # Support the 'restricted' join rule. - restricted_join_rule: bool - # Support for the proper redaction rules for the restricted join rule. This requires - # restricted_join_rule to be enabled. - restricted_join_rule_fix: bool - # Support the 'knock' join rule. - knock_join_rule: bool - # MSC3389: Protect relation information from redaction. - msc3389_relation_redactions: bool - # Support the 'knock_restricted' join rule. - knock_restricted_join_rule: bool - # Enforce integer power levels - enforce_int_power_levels: bool - # MSC3931: Adds a push rule condition for "room version feature flags", making - # some push rules room version dependent. Note that adding a flag to this list - # is not enough to mark it "supported": the push rule evaluator also needs to - # support the flag. Unknown flags are ignored by the evaluator, making conditions - # fail if used. - msc3931_push_features: tuple[str, ...] # values from PushRuleRoomFlag - # MSC3757: Restricting who can overwrite a state event - msc3757_enabled: bool - # MSC4289: Creator power enabled - msc4289_creator_power_enabled: bool - # MSC4291: Room IDs as hashes of the create event - msc4291_room_ids_as_hashes: bool - - -class RoomVersions: - V1 = RoomVersion( - "1", - RoomDisposition.STABLE, - EventFormatVersions.ROOM_V1_V2, - StateResolutionVersions.V1, - enforce_key_validity=False, - special_case_aliases_auth=True, - strict_canonicaljson=False, - limit_notifications_power_levels=False, - implicit_room_creator=False, - updated_redaction_rules=False, - restricted_join_rule=False, - restricted_join_rule_fix=False, - knock_join_rule=False, - msc3389_relation_redactions=False, - knock_restricted_join_rule=False, - enforce_int_power_levels=False, - msc3931_push_features=(), - msc3757_enabled=False, - msc4289_creator_power_enabled=False, - msc4291_room_ids_as_hashes=False, - ) - V2 = RoomVersion( - "2", - RoomDisposition.STABLE, - EventFormatVersions.ROOM_V1_V2, - StateResolutionVersions.V2, - enforce_key_validity=False, - special_case_aliases_auth=True, - strict_canonicaljson=False, - limit_notifications_power_levels=False, - implicit_room_creator=False, - updated_redaction_rules=False, - restricted_join_rule=False, - restricted_join_rule_fix=False, - knock_join_rule=False, - msc3389_relation_redactions=False, - knock_restricted_join_rule=False, - enforce_int_power_levels=False, - msc3931_push_features=(), - msc3757_enabled=False, - msc4289_creator_power_enabled=False, - msc4291_room_ids_as_hashes=False, - ) - V3 = RoomVersion( - "3", - RoomDisposition.STABLE, - EventFormatVersions.ROOM_V3, - StateResolutionVersions.V2, - enforce_key_validity=False, - special_case_aliases_auth=True, - strict_canonicaljson=False, - limit_notifications_power_levels=False, - implicit_room_creator=False, - updated_redaction_rules=False, - restricted_join_rule=False, - restricted_join_rule_fix=False, - knock_join_rule=False, - msc3389_relation_redactions=False, - knock_restricted_join_rule=False, - enforce_int_power_levels=False, - msc3931_push_features=(), - msc3757_enabled=False, - msc4289_creator_power_enabled=False, - msc4291_room_ids_as_hashes=False, - ) - V4 = RoomVersion( - "4", - RoomDisposition.STABLE, - EventFormatVersions.ROOM_V4_PLUS, - StateResolutionVersions.V2, - enforce_key_validity=False, - special_case_aliases_auth=True, - strict_canonicaljson=False, - limit_notifications_power_levels=False, - implicit_room_creator=False, - updated_redaction_rules=False, - restricted_join_rule=False, - restricted_join_rule_fix=False, - knock_join_rule=False, - msc3389_relation_redactions=False, - knock_restricted_join_rule=False, - enforce_int_power_levels=False, - msc3931_push_features=(), - msc3757_enabled=False, - msc4289_creator_power_enabled=False, - msc4291_room_ids_as_hashes=False, - ) - V5 = RoomVersion( - "5", - RoomDisposition.STABLE, - EventFormatVersions.ROOM_V4_PLUS, - StateResolutionVersions.V2, - enforce_key_validity=True, - special_case_aliases_auth=True, - strict_canonicaljson=False, - limit_notifications_power_levels=False, - implicit_room_creator=False, - updated_redaction_rules=False, - restricted_join_rule=False, - restricted_join_rule_fix=False, - knock_join_rule=False, - msc3389_relation_redactions=False, - knock_restricted_join_rule=False, - enforce_int_power_levels=False, - msc3931_push_features=(), - msc3757_enabled=False, - msc4289_creator_power_enabled=False, - msc4291_room_ids_as_hashes=False, - ) - V6 = RoomVersion( - "6", - RoomDisposition.STABLE, - EventFormatVersions.ROOM_V4_PLUS, - StateResolutionVersions.V2, - enforce_key_validity=True, - special_case_aliases_auth=False, - strict_canonicaljson=True, - limit_notifications_power_levels=True, - implicit_room_creator=False, - updated_redaction_rules=False, - restricted_join_rule=False, - restricted_join_rule_fix=False, - knock_join_rule=False, - msc3389_relation_redactions=False, - knock_restricted_join_rule=False, - enforce_int_power_levels=False, - msc3931_push_features=(), - msc3757_enabled=False, - msc4289_creator_power_enabled=False, - msc4291_room_ids_as_hashes=False, - ) - V7 = RoomVersion( - "7", - RoomDisposition.STABLE, - EventFormatVersions.ROOM_V4_PLUS, - StateResolutionVersions.V2, - enforce_key_validity=True, - special_case_aliases_auth=False, - strict_canonicaljson=True, - limit_notifications_power_levels=True, - implicit_room_creator=False, - updated_redaction_rules=False, - restricted_join_rule=False, - restricted_join_rule_fix=False, - knock_join_rule=True, - msc3389_relation_redactions=False, - knock_restricted_join_rule=False, - enforce_int_power_levels=False, - msc3931_push_features=(), - msc3757_enabled=False, - msc4289_creator_power_enabled=False, - msc4291_room_ids_as_hashes=False, - ) - V8 = RoomVersion( - "8", - RoomDisposition.STABLE, - EventFormatVersions.ROOM_V4_PLUS, - StateResolutionVersions.V2, - enforce_key_validity=True, - special_case_aliases_auth=False, - strict_canonicaljson=True, - limit_notifications_power_levels=True, - implicit_room_creator=False, - updated_redaction_rules=False, - restricted_join_rule=True, - restricted_join_rule_fix=False, - knock_join_rule=True, - msc3389_relation_redactions=False, - knock_restricted_join_rule=False, - enforce_int_power_levels=False, - msc3931_push_features=(), - msc3757_enabled=False, - msc4289_creator_power_enabled=False, - msc4291_room_ids_as_hashes=False, - ) - V9 = RoomVersion( - "9", - RoomDisposition.STABLE, - EventFormatVersions.ROOM_V4_PLUS, - StateResolutionVersions.V2, - enforce_key_validity=True, - special_case_aliases_auth=False, - strict_canonicaljson=True, - limit_notifications_power_levels=True, - implicit_room_creator=False, - updated_redaction_rules=False, - restricted_join_rule=True, - restricted_join_rule_fix=True, - knock_join_rule=True, - msc3389_relation_redactions=False, - knock_restricted_join_rule=False, - enforce_int_power_levels=False, - msc3931_push_features=(), - msc3757_enabled=False, - msc4289_creator_power_enabled=False, - msc4291_room_ids_as_hashes=False, - ) - V10 = RoomVersion( - "10", - RoomDisposition.STABLE, - EventFormatVersions.ROOM_V4_PLUS, - StateResolutionVersions.V2, - enforce_key_validity=True, - special_case_aliases_auth=False, - strict_canonicaljson=True, - limit_notifications_power_levels=True, - implicit_room_creator=False, - updated_redaction_rules=False, - restricted_join_rule=True, - restricted_join_rule_fix=True, - knock_join_rule=True, - msc3389_relation_redactions=False, - knock_restricted_join_rule=True, - enforce_int_power_levels=True, - msc3931_push_features=(), - msc3757_enabled=False, - msc4289_creator_power_enabled=False, - msc4291_room_ids_as_hashes=False, - ) - MSC1767v10 = RoomVersion( - # MSC1767 (Extensible Events) based on room version "10" - "org.matrix.msc1767.10", - RoomDisposition.UNSTABLE, - EventFormatVersions.ROOM_V4_PLUS, - StateResolutionVersions.V2, - enforce_key_validity=True, - special_case_aliases_auth=False, - strict_canonicaljson=True, - limit_notifications_power_levels=True, - implicit_room_creator=False, - updated_redaction_rules=False, - restricted_join_rule=True, - restricted_join_rule_fix=True, - knock_join_rule=True, - msc3389_relation_redactions=False, - knock_restricted_join_rule=True, - enforce_int_power_levels=True, - msc3931_push_features=(PushRuleRoomFlag.EXTENSIBLE_EVENTS,), - msc3757_enabled=False, - msc4289_creator_power_enabled=False, - msc4291_room_ids_as_hashes=False, - ) - MSC3757v10 = RoomVersion( - # MSC3757 (Restricting who can overwrite a state event) based on room version "10" - "org.matrix.msc3757.10", - RoomDisposition.UNSTABLE, - EventFormatVersions.ROOM_V4_PLUS, - StateResolutionVersions.V2, - enforce_key_validity=True, - special_case_aliases_auth=False, - strict_canonicaljson=True, - limit_notifications_power_levels=True, - implicit_room_creator=False, - updated_redaction_rules=False, - restricted_join_rule=True, - restricted_join_rule_fix=True, - knock_join_rule=True, - msc3389_relation_redactions=False, - knock_restricted_join_rule=True, - enforce_int_power_levels=True, - msc3931_push_features=(), - msc3757_enabled=True, - msc4289_creator_power_enabled=False, - msc4291_room_ids_as_hashes=False, - ) - V11 = RoomVersion( - "11", - RoomDisposition.STABLE, - EventFormatVersions.ROOM_V4_PLUS, - StateResolutionVersions.V2, - enforce_key_validity=True, - special_case_aliases_auth=False, - strict_canonicaljson=True, - limit_notifications_power_levels=True, - implicit_room_creator=True, # Used by MSC3820 - updated_redaction_rules=True, # Used by MSC3820 - restricted_join_rule=True, - restricted_join_rule_fix=True, - knock_join_rule=True, - msc3389_relation_redactions=False, - knock_restricted_join_rule=True, - enforce_int_power_levels=True, - msc3931_push_features=(), - msc3757_enabled=False, - msc4289_creator_power_enabled=False, - msc4291_room_ids_as_hashes=False, - ) - MSC3757v11 = RoomVersion( - # MSC3757 (Restricting who can overwrite a state event) based on room version "11" - "org.matrix.msc3757.11", - RoomDisposition.UNSTABLE, - EventFormatVersions.ROOM_V4_PLUS, - StateResolutionVersions.V2, - enforce_key_validity=True, - special_case_aliases_auth=False, - strict_canonicaljson=True, - limit_notifications_power_levels=True, - implicit_room_creator=True, # Used by MSC3820 - updated_redaction_rules=True, # Used by MSC3820 - restricted_join_rule=True, - restricted_join_rule_fix=True, - knock_join_rule=True, - msc3389_relation_redactions=False, - knock_restricted_join_rule=True, - enforce_int_power_levels=True, - msc3931_push_features=(), - msc3757_enabled=True, - msc4289_creator_power_enabled=False, - msc4291_room_ids_as_hashes=False, - ) - HydraV11 = RoomVersion( - "org.matrix.hydra.11", - RoomDisposition.UNSTABLE, - EventFormatVersions.ROOM_V11_HYDRA_PLUS, - StateResolutionVersions.V2_1, # Changed from v11 - enforce_key_validity=True, - special_case_aliases_auth=False, - strict_canonicaljson=True, - limit_notifications_power_levels=True, - implicit_room_creator=True, # Used by MSC3820 - updated_redaction_rules=True, # Used by MSC3820 - restricted_join_rule=True, - restricted_join_rule_fix=True, - knock_join_rule=True, - msc3389_relation_redactions=False, - knock_restricted_join_rule=True, - enforce_int_power_levels=True, - msc3931_push_features=(), - msc3757_enabled=False, - msc4289_creator_power_enabled=True, # Changed from v11 - msc4291_room_ids_as_hashes=True, # Changed from v11 - ) - V12 = RoomVersion( - "12", - RoomDisposition.STABLE, - EventFormatVersions.ROOM_V11_HYDRA_PLUS, - StateResolutionVersions.V2_1, # Changed from v11 - enforce_key_validity=True, - special_case_aliases_auth=False, - strict_canonicaljson=True, - limit_notifications_power_levels=True, - implicit_room_creator=True, # Used by MSC3820 - updated_redaction_rules=True, # Used by MSC3820 - restricted_join_rule=True, - restricted_join_rule_fix=True, - knock_join_rule=True, - msc3389_relation_redactions=False, - knock_restricted_join_rule=True, - enforce_int_power_levels=True, - msc3931_push_features=(), - msc3757_enabled=False, - msc4289_creator_power_enabled=True, # Changed from v11 - msc4291_room_ids_as_hashes=True, # Changed from v11 - ) - - -KNOWN_ROOM_VERSIONS: dict[str, RoomVersion] = { - v.identifier: v - for v in ( - RoomVersions.V1, - RoomVersions.V2, - RoomVersions.V3, - RoomVersions.V4, - RoomVersions.V5, - RoomVersions.V6, - RoomVersions.V7, - RoomVersions.V8, - RoomVersions.V9, - RoomVersions.V10, - RoomVersions.V11, - RoomVersions.V12, - RoomVersions.MSC3757v10, - RoomVersions.MSC3757v11, - RoomVersions.HydraV11, - ) -} +from synapse.synapse_rust.room_versions import ( + KNOWN_EVENT_FORMAT_VERSIONS, + KNOWN_ROOM_VERSIONS, + EventFormatVersions, + PushRuleRoomFlag, + RoomVersion, + RoomVersions, + StateResolutionVersions, +) + +__all__ = [ + "EventFormatVersions", + "KNOWN_EVENT_FORMAT_VERSIONS", + "KNOWN_ROOM_VERSIONS", + "PushRuleRoomFlag", + "RoomVersion", + "RoomVersions", + "StateResolutionVersions", +] diff --git a/synapse/config/experimental.py b/synapse/config/experimental.py index ff9b394eb9e..e77412a7974 100644 --- a/synapse/config/experimental.py +++ b/synapse/config/experimental.py @@ -477,8 +477,7 @@ def read_config( self.msc1767_enabled: bool = experimental.get("msc1767_enabled", False) if self.msc1767_enabled: # Enable room version (and thus applicable push rules from MSC3931/3932) - version_id = RoomVersions.MSC1767v10.identifier - KNOWN_ROOM_VERSIONS[version_id] = RoomVersions.MSC1767v10 + KNOWN_ROOM_VERSIONS.add_room_version(RoomVersions.MSC1767v10) # MSC3391: Removing account data. self.msc3391_enabled = experimental.get("msc3391_enabled", False) diff --git a/synapse/event_auth.py b/synapse/event_auth.py index 7098843742e..f39f55b472d 100644 --- a/synapse/event_auth.py +++ b/synapse/event_auth.py @@ -60,7 +60,6 @@ KNOWN_ROOM_VERSIONS, EventFormatVersions, RoomVersion, - RoomVersions, ) from synapse.events import is_creator from synapse.state import CREATE_KEY @@ -383,25 +382,6 @@ def check_state_dependent_auth_rules( logger.debug("Allowing! %s", event) -# Set of room versions where Synapse did not apply event key size limits -# in bytes, but rather in codepoints. -# In these room versions, we are more lenient with event size validation. -LENIENT_EVENT_BYTE_LIMITS_ROOM_VERSIONS = { - RoomVersions.V1, - RoomVersions.V2, - RoomVersions.V3, - RoomVersions.V4, - RoomVersions.V5, - RoomVersions.V6, - RoomVersions.V7, - RoomVersions.V8, - RoomVersions.V9, - RoomVersions.V10, - RoomVersions.MSC1767v10, - RoomVersions.MSC3757v10, -} - - def _check_size_limits(event: "EventBase") -> None: """ Checks the size limits in a PDU. @@ -440,9 +420,7 @@ def _check_size_limits(event: "EventBase") -> None: if len(event.event_id) > 255: raise EventSizeError("'event_id' too large", unpersistable=True) - strict_byte_limits = ( - event.room_version not in LENIENT_EVENT_BYTE_LIMITS_ROOM_VERSIONS - ) + strict_byte_limits = event.room_version.strict_event_byte_limits_room_versions # Byte size check: if these fail, then be lenient to avoid breaking rooms. if len(event.user_id.encode("utf-8")) > 255: diff --git a/synapse/synapse_rust/push.pyi b/synapse/synapse_rust/push.pyi index 9d8f0389e89..ef0d5f94f4f 100644 --- a/synapse/synapse_rust/push.pyi +++ b/synapse/synapse_rust/push.pyi @@ -65,7 +65,7 @@ class PushRuleEvaluator: notification_power_levels: Mapping[str, int], related_events_flattened: Mapping[str, Mapping[str, JsonValue]], related_event_match_enabled: bool, - room_version_feature_flags: tuple[str, ...], + room_version_feature_flags: list[str], msc3931_enabled: bool, msc4210_enabled: bool, msc4306_enabled: bool, diff --git a/synapse/synapse_rust/room_versions.pyi b/synapse/synapse_rust/room_versions.pyi new file mode 100644 index 00000000000..909e3a1c26f --- /dev/null +++ b/synapse/synapse_rust/room_versions.pyi @@ -0,0 +1,142 @@ +# This file is licensed under the Affero General Public License (AGPL) version 3. +# +# Copyright (C) 2026 Element Creations Ltd. +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# See the GNU Affero General Public License for more details: +# . + +from collections.abc import Mapping +from typing import Iterator + +class EventFormatVersions: + """Internal enum for tracking the version of the event format, + independently of the room version. + + To reduce confusion, the event format versions are named after the room + versions that they were used or introduced in. + The concept of an 'event format version' is specific to Synapse (the + specification does not mention this term.) + """ + + ROOM_V1_V2: int + """:server event id format: used for room v1 and v2""" + ROOM_V3: int + """MSC1659-style event id format: used for room v3""" + ROOM_V4_PLUS: int + """MSC1884-style format: introduced for room v4""" + ROOM_V11_HYDRA_PLUS: int + """MSC4291 room IDs as hashes: introduced for room HydraV11""" + +KNOWN_EVENT_FORMAT_VERSIONS: frozenset[int] + +class StateResolutionVersions: + """Enum to identify the state resolution algorithms.""" + + V1: int + """Room v1 state res""" + V2: int + """MSC1442 state res: room v2 and later""" + V2_1: int + """MSC4297 state res""" + +class RoomDisposition: + """Room disposition constants.""" + + STABLE: str + UNSTABLE: str + +class PushRuleRoomFlag: + """Enum for listing possible MSC3931 room version feature flags, for push rules.""" + + EXTENSIBLE_EVENTS: str + """MSC3932: Room version supports MSC1767 Extensible Events.""" + +class RoomVersion: + """An object which describes the unique attributes of a room version.""" + + identifier: str + """The identifier for this version.""" + disposition: str + """One of the RoomDisposition constants.""" + event_format: int + """One of the EventFormatVersions constants.""" + state_res: int + """One of the StateResolutionVersions constants.""" + enforce_key_validity: bool + special_case_aliases_auth: bool + """Before MSC2432, m.room.aliases had special auth rules and redaction rules.""" + strict_canonicaljson: bool + """Strictly enforce canonicaljson, do not allow: + * Integers outside the range of [-2^53 + 1, 2^53 - 1] + * Floats + * NaN, Infinity, -Infinity + """ + limit_notifications_power_levels: bool + """MSC2209: Check 'notifications' key while verifying + m.room.power_levels auth rules.""" + implicit_room_creator: bool + """MSC3820: No longer include the creator in m.room.create events (room version 11).""" + updated_redaction_rules: bool + """MSC3820: Apply updated redaction rules algorithm from room version 11.""" + restricted_join_rule: bool + """Support the 'restricted' join rule.""" + restricted_join_rule_fix: bool + """Support for the proper redaction rules for the restricted join rule. + This requires restricted_join_rule to be enabled.""" + knock_join_rule: bool + """Support the 'knock' join rule.""" + msc3389_relation_redactions: bool + """MSC3389: Protect relation information from redaction.""" + knock_restricted_join_rule: bool + """Support the 'knock_restricted' join rule.""" + enforce_int_power_levels: bool + """Enforce integer power levels.""" + msc3931_push_features: list[str] + """MSC3931: Adds a push rule condition for "room version feature flags", making + some push rules room version dependent. Note that adding a flag to this list + is not enough to mark it "supported": the push rule evaluator also needs to + support the flag. Unknown flags are ignored by the evaluator, making conditions + fail if used. Values from PushRuleRoomFlag.""" + msc3757_enabled: bool + """MSC3757: Restricting who can overwrite a state event.""" + msc4289_creator_power_enabled: bool + """MSC4289: Creator power enabled.""" + msc4291_room_ids_as_hashes: bool + """MSC4291: Room IDs as hashes of the create event.""" + strict_event_byte_limits_room_versions: bool + """Whether this room version strictly enforces event key size limits in bytes, + rather than in codepoints. + + If true, this room version uses stricter event size validation.""" + +class RoomVersions: + V1: RoomVersion + V2: RoomVersion + V3: RoomVersion + V4: RoomVersion + V5: RoomVersion + V6: RoomVersion + V7: RoomVersion + V8: RoomVersion + V9: RoomVersion + V10: RoomVersion + MSC1767v10: RoomVersion + MSC3389v10: RoomVersion + MSC3757v10: RoomVersion + V11: RoomVersion + MSC3757v11: RoomVersion + HydraV11: RoomVersion + V12: RoomVersion + +class KnownRoomVersionsMapping(Mapping[str, RoomVersion]): + def add_room_version(self, room_version: RoomVersion) -> None: ... + def __getitem__(self, key: str) -> RoomVersion: ... + def __len__(self) -> int: ... + def __iter__(self) -> Iterator[str]: ... + +KNOWN_ROOM_VERSIONS: KnownRoomVersionsMapping diff --git a/tests/events/test_utils.py b/tests/events/test_utils.py index 9ea015e1381..f511f577d31 100644 --- a/tests/events/test_utils.py +++ b/tests/events/test_utils.py @@ -22,7 +22,6 @@ import unittest as stdlib_unittest from typing import Any, Mapping -import attr from parameterized import parameterized from synapse.api.constants import EventContentFields @@ -553,11 +552,6 @@ def test_relations(self) -> None: room_version=RoomVersions.V10, ) - # Create a new room version. - msc3389_room_ver = attr.evolve( - RoomVersions.V10, msc3389_relation_redactions=True - ) - self.run_test( { "type": "m.room.message", @@ -581,7 +575,7 @@ def test_relations(self) -> None: "signatures": {}, "unsigned": {}, }, - room_version=msc3389_room_ver, + room_version=RoomVersions.MSC3389v10, ) # If the field is not an object, redact it. @@ -599,7 +593,7 @@ def test_relations(self) -> None: "signatures": {}, "unsigned": {}, }, - room_version=msc3389_room_ver, + room_version=RoomVersions.MSC3389v10, ) # If the m.relates_to property would be empty, redact it. @@ -614,7 +608,7 @@ def test_relations(self) -> None: "signatures": {}, "unsigned": {}, }, - room_version=msc3389_room_ver, + room_version=RoomVersions.MSC3389v10, ) From 539f708f32af4cc01f6d82f1bb0069ff38e93593 Mon Sep 17 00:00:00 2001 From: Erik Johnston Date: Thu, 26 Mar 2026 09:18:08 +0000 Subject: [PATCH 102/121] Remove `redacted_because` from internal unsigned. (#19581) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This is a simplification so that `unsigned` only includes "simple" values, to make it easier to port to Rust. Reviewable commit-by-commit Summary: 1. **Add `recheck` column to `redactions` table** A new boolean `recheck` column (default true) is added to the `redactions` table. This captures whether a redaction needs its sender domain checked at read time — required for room v3+ where redactions are accepted speculatively and later validated. When persisting a new redaction, `recheck` is set directly from `event.internal_metadata.need_to_check_redaction()`. It's fine if initially we recheck all redactions, as it only results in a little more CPU overhead (as we always pull out the redaction event regardless). 2. **Backfill `recheck` via background update** A background update (`redactions_recheck`) backfills the new column for existing rows by reading `recheck_redaction` from each event's `internal_metadata` JSON. This avoids loading full event objects by reading `event_json` directly via a SQL JOIN. 3. **Don't fetch confirmed redaction events from the DB** Previously, when loading events, Synapse recursively fetched all redaction events regardless of whether they needed domain rechecking. Now `_fetch_event_rows` reads the `recheck` column and splits redactions into two lists: - `unconfirmed_redactions` — need fetching and domain validation - `confirmed_redactions` — already validated, applied directly without fetching the event This avoids unnecessary DB reads for the common case of already-confirmed redactions. 4. **Move `redacted_because` population to `EventClientSerializer`** Previously, `redacted_because` (the full redaction event object) was stored in `event.unsigned` at DB fetch time, coupling storage-layer code to client serialization concerns. This is removed from `_maybe_redact_event_row` and moved into `EventClientSerializer.serialize_event`, which fetches the redaction event on demand. The storage layer now only sets `unsigned["redacted_by"]` (the redaction event ID). 5. **Always use `EventClientSerializer`** The standalone `serialize_event` function was made private (`_serialize_event`). All external callers — `rest/client/room.py`, `rest/admin/events.py, appservice/api.py`, and `tests` — were updated to use `EventClientSerializer.serialize_event` / `serialize_events`, ensuring `redacted_because` is always populated correctly via the serializer. 6. **Batch-fetch redaction events in `serialize_events`** `serialize_events` now collects all `redacted_by` IDs from the event batch upfront and fetches them in a single `get_events` call, passing the result as a `redaction_map` to each `serialize_event` call. This reduces N individual DB round-trips to one when serializing a batch of events that includes redacted events. --------- Co-authored-by: Andrew Morgan <1342360+anoadragon453@users.noreply.github.com> Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- changelog.d/19581.misc | 1 + rust/src/events/internal_metadata.rs | 6 + synapse/_scripts/synapse_port_db.py | 2 +- synapse/appservice/api.py | 48 +++---- synapse/events/utils.py | 81 ++++++++--- synapse/rest/admin/events.py | 8 +- synapse/rest/client/room.py | 4 +- synapse/storage/databases/main/events.py | 1 + .../databases/main/events_bg_updates.py | 65 +++++++++ .../storage/databases/main/events_worker.py | 67 ++++++--- synapse/storage/schema/__init__.py | 5 +- .../main/delta/94/01_redactions_recheck.sql | 15 ++ .../94/02_redactions_recheck_bg_update.sql | 15 ++ synapse/synapse_rust/events.pyi | 2 + synapse/types/storage/__init__.py | 2 + tests/events/test_utils.py | 81 +++++++++-- tests/replication/storage/test_events.py | 6 +- tests/rest/client/test_rooms.py | 10 +- tests/storage/test_events_bg_updates.py | 133 ++++++++++++++++++ tests/storage/test_redaction.py | 35 +---- 20 files changed, 466 insertions(+), 121 deletions(-) create mode 100644 changelog.d/19581.misc create mode 100644 synapse/storage/schema/main/delta/94/01_redactions_recheck.sql create mode 100644 synapse/storage/schema/main/delta/94/02_redactions_recheck_bg_update.sql diff --git a/changelog.d/19581.misc b/changelog.d/19581.misc new file mode 100644 index 00000000000..02856f34978 --- /dev/null +++ b/changelog.d/19581.misc @@ -0,0 +1 @@ +Remove `redacted_because` from internal unsigned. diff --git a/rust/src/events/internal_metadata.rs b/rust/src/events/internal_metadata.rs index 595f9cf7eb7..a53c1e771b6 100644 --- a/rust/src/events/internal_metadata.rs +++ b/rust/src/events/internal_metadata.rs @@ -261,6 +261,11 @@ pub struct EventInternalMetadata { #[pyo3(get, set)] instance_name: Option, + /// The event ID of the redaction event, if this event has been redacted. + /// This is set dynamically at load time and is not persisted to the database. + #[pyo3(get, set)] + redacted_by: Option, + /// whether this event is an outlier (ie, whether we have the state at that /// point in the DAG) #[pyo3(get, set)] @@ -289,6 +294,7 @@ impl EventInternalMetadata { data, stream_ordering: None, instance_name: None, + redacted_by: None, outlier: false, }) } diff --git a/synapse/_scripts/synapse_port_db.py b/synapse/_scripts/synapse_port_db.py index 79b2a0c528e..eedceb170e9 100755 --- a/synapse/_scripts/synapse_port_db.py +++ b/synapse/_scripts/synapse_port_db.py @@ -122,7 +122,7 @@ "presence_stream": ["currently_active"], "public_room_list_stream": ["visibility"], "pushers": ["enabled"], - "redactions": ["have_censored"], + "redactions": ["have_censored", "recheck"], "remote_media_cache": ["authenticated"], "room_memberships": ["participant"], "room_stats_state": ["is_federatable"], diff --git a/synapse/appservice/api.py b/synapse/appservice/api.py index 2bbf77a352c..d4e9d50b966 100644 --- a/synapse/appservice/api.py +++ b/synapse/appservice/api.py @@ -32,7 +32,7 @@ from prometheus_client import Counter from typing_extensions import ParamSpec, TypeGuard -from synapse.api.constants import EventTypes, Membership, ThirdPartyEntityKind +from synapse.api.constants import ThirdPartyEntityKind from synapse.api.errors import CodeMessageException, HttpResponseException from synapse.appservice import ( ApplicationService, @@ -40,7 +40,7 @@ TransactionUnusedFallbackKeys, ) from synapse.events import EventBase -from synapse.events.utils import SerializeEventConfig, serialize_event +from synapse.events.utils import SerializeEventConfig from synapse.http.client import SimpleHttpClient, is_unknown_endpoint from synapse.logging import opentracing from synapse.metrics import SERVER_NAME_LABEL @@ -128,6 +128,7 @@ def __init__(self, hs: "HomeServer"): self.server_name = hs.hostname self.clock = hs.get_clock() self.config = hs.config.appservice + self._event_serializer = hs.get_event_client_serializer() self.protocol_meta_cache: ResponseCache[tuple[str, str]] = ResponseCache( clock=hs.get_clock(), @@ -343,7 +344,7 @@ async def push_bulk( # This is required by the configuration. assert service.hs_token is not None - serialized_events = self._serialize(service, events) + serialized_events = await self._serialize(service, events) if txn_id is None: logger.warning( @@ -539,30 +540,23 @@ async def query_keys( return response - def _serialize( + async def _serialize( self, service: "ApplicationService", events: Iterable[EventBase] ) -> list[JsonDict]: time_now = self.clock.time_msec() - return [ - serialize_event( - e, - time_now, - config=SerializeEventConfig( - as_client_event=True, - # If this is an invite or a knock membership event, and we're interested - # in this user, then include any stripped state alongside the event. - include_stripped_room_state=( - e.type == EventTypes.Member - and ( - e.membership == Membership.INVITE - or e.membership == Membership.KNOCK - ) - and service.is_interested_in_user(e.state_key) - ), - # Appservices are considered 'trusted' by the admin and should have - # applicable metadata on their events. - include_admin_metadata=True, - ), - ) - for e in events - ] + return await self._event_serializer.serialize_events( + list(events), + time_now, + config=SerializeEventConfig( + as_client_event=True, + # If this is an invite or a knock membership event, then include + # any stripped state alongside the event. We could narrow this + # down to only users the appservice is "interested in", however + # it's not worth the complexity of doing so, and it's simpler to + # just include it for all users. + include_stripped_room_state=True, + # Appservices are considered 'trusted' by the admin and should have + # applicable metadata on their events. + include_admin_metadata=True, + ), + ) diff --git a/synapse/events/utils.py b/synapse/events/utils.py index 89eb2182afd..76ebac8b17c 100644 --- a/synapse/events/utils.py +++ b/synapse/events/utils.py @@ -88,6 +88,7 @@ def prune_event(event: EventBase) -> EventBase: ) pruned_event.internal_metadata.instance_name = event.internal_metadata.instance_name pruned_event.internal_metadata.outlier = event.internal_metadata.outlier + pruned_event.internal_metadata.redacted_by = event.internal_metadata.redacted_by # Mark the event as redacted pruned_event.internal_metadata.redacted = True @@ -123,6 +124,7 @@ def clone_event(event: EventBase) -> EventBase: ) new_event.internal_metadata.instance_name = event.internal_metadata.instance_name new_event.internal_metadata.outlier = event.internal_metadata.outlier + new_event.internal_metadata.redacted_by = event.internal_metadata.redacted_by return new_event @@ -423,7 +425,7 @@ class SerializeEventConfig: # 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 + only_event_fields: list[str] | None = attr.ib(default=None) # Some events can have stripped room state stored in the `unsigned` field. # This is required for invite and knock functionality. If this option is # False, that state will be removed from the event before it is returned. @@ -434,6 +436,16 @@ class SerializeEventConfig: # whether an event was soft failed by the server. include_admin_metadata: bool = False + @only_event_fields.validator + def _validate_only_event_fields( + self, attribute: attr.Attribute, value: Any + ) -> None: + if value is None: + return + + if not isinstance(value, list) or not all(isinstance(f, str) for f in value): + raise TypeError("only_event_fields must be a list of strings") + _DEFAULT_SERIALIZE_EVENT_CONFIG = SerializeEventConfig() @@ -444,7 +456,7 @@ def make_config_for_admin(existing: SerializeEventConfig) -> SerializeEventConfi return attr.evolve(existing, include_admin_metadata=True) -def serialize_event( +def _serialize_event( e: JsonDict | EventBase, time_now_ms: int, *, @@ -476,13 +488,6 @@ def serialize_event( d["unsigned"]["age"] = time_now_ms - d["unsigned"]["age_ts"] del d["unsigned"]["age_ts"] - if "redacted_because" in e.unsigned: - d["unsigned"]["redacted_because"] = serialize_event( - e.unsigned["redacted_because"], - time_now_ms, - config=config, - ) - # 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. @@ -559,14 +564,6 @@ def serialize_event( if e.internal_metadata.policy_server_spammy: d["unsigned"]["io.element.synapse.policy_server_spammy"] = True - only_event_fields = config.only_event_fields - if only_event_fields: - if not isinstance(only_event_fields, list) or not all( - isinstance(f, str) for f in only_event_fields - ): - raise TypeError("only_event_fields must be a list of strings") - d = only_fields(d, only_event_fields) - return d @@ -591,6 +588,7 @@ async def serialize_event( *, config: SerializeEventConfig = _DEFAULT_SERIALIZE_EVENT_CONFIG, bundle_aggregations: dict[str, "BundledAggregations"] | None = None, + redaction_map: Mapping[str, "EventBase"] | None = None, ) -> JsonDict: """Serializes a single event. @@ -600,6 +598,8 @@ async def serialize_event( config: Event serialization config bundle_aggregations: A map from event_id to the aggregations to be bundled into the event. + redaction_map: Optional pre-fetched map from redaction event_id to event, + used to avoid per-event DB lookups when serializing many events. Returns: The serialized event @@ -617,7 +617,34 @@ async def serialize_event( ): config = make_config_for_admin(config) - serialized_event = serialize_event(event, time_now, config=config) + serialized_event = _serialize_event(event, time_now, config=config) + + # If the event was redacted, fetch the redaction event from the database + # and include it in the serialized event's unsigned section. + redacted_by: str | None = event.internal_metadata.redacted_by + if redacted_by is not None: + serialized_event.setdefault("unsigned", {})["redacted_by"] = redacted_by + if redaction_map is not None: + redaction_event: EventBase | None = redaction_map.get(redacted_by) + else: + redaction_event = await self._store.get_event( + redacted_by, + allow_none=True, + ) + if redaction_event is not None: + serialized_redaction = _serialize_event( + redaction_event, time_now, config=config + ) + serialized_event.setdefault("unsigned", {})["redacted_because"] = ( + serialized_redaction + ) + # format_event_for_client_v1 copies redacted_because to the + # top level, but since we add it after that runs, do it here. + if ( + config.as_client_event + and config.event_format is format_event_for_client_v1 + ): + serialized_event["redacted_because"] = serialized_redaction new_unsigned = {} for callback in self._add_extra_fields_to_unsigned_client_event_callbacks: @@ -630,6 +657,13 @@ async def serialize_event( new_unsigned.update(serialized_event["unsigned"]) serialized_event["unsigned"] = new_unsigned + # Only include fields that the client has requested. + # + # Note: we always return bundled aggregations, though it is unclear why. + only_event_fields = config.only_event_fields + if only_event_fields: + serialized_event = only_fields(serialized_event, only_event_fields) + # Check if there are any bundled aggregations to include with the event. if bundle_aggregations: if event.event_id in bundle_aggregations: @@ -745,12 +779,23 @@ async def serialize_events( str(len(events)), ) + # Batch-fetch all redaction events in one go rather than one per event. + redaction_ids = { + e.internal_metadata.redacted_by + for e in events + if isinstance(e, EventBase) and e.internal_metadata.redacted_by is not None + } + redaction_map = ( + await self._store.get_events(redaction_ids) if redaction_ids else {} + ) + return [ await self.serialize_event( event, time_now, config=config, bundle_aggregations=bundle_aggregations, + redaction_map=redaction_map, ) for event in events ] diff --git a/synapse/rest/admin/events.py b/synapse/rest/admin/events.py index 1c39d5caf30..8da7a67820a 100644 --- a/synapse/rest/admin/events.py +++ b/synapse/rest/admin/events.py @@ -5,7 +5,6 @@ from synapse.events.utils import ( SerializeEventConfig, format_event_raw, - serialize_event, ) from synapse.http.servlet import RestServlet from synapse.http.site import SynapseRequest @@ -40,6 +39,7 @@ def __init__(self, hs: "HomeServer"): self._auth = hs.get_auth() self._store = hs.get_datastores().main self._clock = hs.get_clock() + self._event_serializer = hs.get_event_client_serializer() async def on_GET( self, request: SynapseRequest, event_id: str @@ -64,6 +64,10 @@ async def on_GET( include_stripped_room_state=True, include_admin_metadata=True, ) - res = {"event": serialize_event(event, self._clock.time_msec(), config=config)} + res = { + "event": await self._event_serializer.serialize_event( + event, self._clock.time_msec(), config=config + ) + } return HTTPStatus.OK, res diff --git a/synapse/rest/client/room.py b/synapse/rest/client/room.py index 9172bfcb4ed..65d9c130efc 100644 --- a/synapse/rest/client/room.py +++ b/synapse/rest/client/room.py @@ -55,7 +55,6 @@ EventClientSerializer, SerializeEventConfig, format_event_for_client_v2, - serialize_event, ) from synapse.handlers.pagination import GetMessagesResult from synapse.http.server import HttpServer @@ -214,6 +213,7 @@ def __init__(self, hs: "HomeServer"): self.delayed_events_handler = hs.get_delayed_events_handler() self.auth = hs.get_auth() self.clock = hs.get_clock() + self._event_serializer = hs.get_event_client_serializer() self._max_event_delay_ms = hs.config.server.max_event_delay_ms self._spam_checker_module_callbacks = hs.get_module_api_callbacks().spam_checker self._msc4354_enabled = hs.config.experimental.msc4354_enabled @@ -285,7 +285,7 @@ async def on_GET( raise SynapseError(404, "Event not found.", errcode=Codes.NOT_FOUND) if format == "event": - event = serialize_event( + event = await self._event_serializer.serialize_event( data, self.clock.time_msec(), config=SerializeEventConfig( diff --git a/synapse/storage/databases/main/events.py b/synapse/storage/databases/main/events.py index cb452dbc9b1..941a5f9f3a3 100644 --- a/synapse/storage/databases/main/events.py +++ b/synapse/storage/databases/main/events.py @@ -2944,6 +2944,7 @@ def _store_redaction(self, txn: LoggingTransaction, event: EventBase) -> None: values={ "redacts": event.redacts, "received_ts": self._clock.time_msec(), + "recheck": event.internal_metadata.need_to_check_redaction(), }, ) diff --git a/synapse/storage/databases/main/events_bg_updates.py b/synapse/storage/databases/main/events_bg_updates.py index f8300e016b1..934cd157caf 100644 --- a/synapse/storage/databases/main/events_bg_updates.py +++ b/synapse/storage/databases/main/events_bg_updates.py @@ -159,6 +159,11 @@ def __init__( "redactions_received_ts", self._redactions_received_ts ) + self.db_pool.updates.register_background_update_handler( + _BackgroundUpdates.REDACTIONS_RECHECK_BG_UPDATE, + self._redactions_recheck_bg_update, + ) + # This index gets deleted in `event_fix_redactions_bytes` update self.db_pool.updates.register_background_index_update( "event_fix_redactions_bytes_create_index", @@ -747,6 +752,66 @@ def _redactions_received_ts_txn(txn: LoggingTransaction) -> int: return count + async def _redactions_recheck_bg_update( + self, progress: JsonDict, batch_size: int + ) -> int: + """Fills in the `recheck` column of the `redactions` table based on + the `recheck_redaction` field in each event's internal metadata.""" + last_event_id = progress.get("last_event_id", "") + + def _txn(txn: LoggingTransaction) -> int: + sql = """ + SELECT r.event_id, ej.internal_metadata + FROM redactions AS r + LEFT JOIN event_json AS ej USING (event_id) + WHERE r.event_id > ? + ORDER BY r.event_id ASC + LIMIT ? + """ + txn.execute(sql, (last_event_id, batch_size)) + rows = txn.fetchall() + if not rows: + return 0 + + updates = [] + for event_id, internal_metadata_json in rows: + if internal_metadata_json is not None: + internal_metadata = db_to_json(internal_metadata_json) + recheck = bool(internal_metadata.get("recheck_redaction", False)) + else: + recheck = False + if not recheck: + # Column defaults to true, so we only need to update rows + # where recheck should be false. + updates.append((event_id, recheck)) + + self.db_pool.simple_update_many_txn( + txn, + table="redactions", + key_names=("event_id",), + key_values=[(event_id,) for event_id, _ in updates], + value_names=("recheck",), + value_values=[(recheck,) for _, recheck in updates], + ) + + upper_event_id = rows[-1][0] + self.db_pool.updates._background_update_progress_txn( + txn, + _BackgroundUpdates.REDACTIONS_RECHECK_BG_UPDATE, + {"last_event_id": upper_event_id}, + ) + + return len(rows) + + count = await self.db_pool.runInteraction("_redactions_recheck_bg_update", _txn) + + if not count: + await self.db_pool.updates._end_background_update( + _BackgroundUpdates.REDACTIONS_RECHECK_BG_UPDATE + ) + + return count + async def _event_fix_redactions_bytes( self, progress: JsonDict, batch_size: int ) -> int: diff --git a/synapse/storage/databases/main/events_worker.py b/synapse/storage/databases/main/events_worker.py index cb0764feb8c..cc79b8042bd 100644 --- a/synapse/storage/databases/main/events_worker.py +++ b/synapse/storage/databases/main/events_worker.py @@ -179,7 +179,11 @@ class _EventRow: rejected_reason: if the event was rejected, the reason why. - redactions: a list of event-ids which (claim to) redact this event. + unconfirmed_redactions: a list of event-ids which (claim to) redact this event + and need to be rechecked. + + confirmed_redactions: a list of event-ids which redact this event and have been + confirmed as valid redactions. outlier: True if this event is an outlier. """ @@ -192,7 +196,8 @@ class _EventRow: format_version: int | None room_version_id: str | None rejected_reason: str | None - redactions: list[str] + unconfirmed_redactions: list[str] + confirmed_redactions: list[str] outlier: bool @@ -1359,14 +1364,20 @@ async def _fetch_event_ids_and_get_outstanding_redactions( ) row_map = await self._enqueue_events(event_ids_to_fetch) - # we need to recursively fetch any redactions of those events + # we need to recursively fetch redaction events that require + # rechecking, so we can validate them redaction_ids: set[str] = set() for event_id in event_ids_to_fetch: row = row_map.get(event_id) fetched_event_ids.add(event_id) if row: fetched_events[event_id] = row - redaction_ids.update(row.redactions) + + # If this event only has unconfirmed redactions we fetch + # them from the DB so that we check them to see if any are + # valid. + if not row.confirmed_redactions: + redaction_ids.update(row.unconfirmed_redactions) event_ids_to_fetch = redaction_ids.difference(fetched_event_ids) return event_ids_to_fetch @@ -1510,9 +1521,12 @@ async def _fetch_event_ids_and_get_outstanding_redactions( # the cache entries. result_map: dict[str, EventCacheEntry] = {} for event_id, original_ev in event_map.items(): - redactions = fetched_events[event_id].redactions + row = fetched_events[event_id] redacted_event = self._maybe_redact_event_row( - original_ev, redactions, event_map + original_ev, + row.unconfirmed_redactions, + row.confirmed_redactions, + event_map, ) cache_entry = EventCacheEntry( @@ -1606,21 +1620,25 @@ def _fetch_event_rows( format_version=row[5], room_version_id=row[6], rejected_reason=row[7], - redactions=[], + unconfirmed_redactions=[], + confirmed_redactions=[], outlier=bool(row[8]), # This is an int in SQLite3 ) # check for redactions - redactions_sql = "SELECT event_id, redacts FROM redactions WHERE " + redactions_sql = "SELECT event_id, redacts, recheck FROM redactions WHERE " clause, args = make_in_list_sql_clause(txn.database_engine, "redacts", evs) txn.execute(redactions_sql + clause, args) - for redacter, redacted in txn: + for redacter, redacted, recheck in txn: d = event_dict.get(redacted) if d: - d.redactions.append(redacter) + if recheck: + d.unconfirmed_redactions.append(redacter) + else: + d.confirmed_redactions.append(redacter) # check for MSC4293 redactions to_check = [] @@ -1669,24 +1687,28 @@ def _fetch_event_rows( # backfilled events, as they have a negative stream ordering if e_row.stream_ordering >= redact_end_ordering: continue - e_row.redactions.append(redacting_event_id) + e_row.unconfirmed_redactions.append(redacting_event_id) return event_dict def _maybe_redact_event_row( self, original_ev: EventBase, - redactions: Iterable[str], + unconfirmed_redactions: Iterable[str], + confirmed_redactions: Iterable[str], event_map: dict[str, EventBase], ) -> EventBase | None: - """Given an event object and a list of possible redacting event ids, + """Given an event object and lists of possible redacting event ids, determine whether to honour any of those redactions and if so return a redacted event. Args: original_ev: The original event. - redactions: list of event ids of potential redaction events + unconfirmed_redactions: list of event ids of redaction events that need + domain rechecking (room v3+). + confirmed_redactions: list of event ids of redaction events that have + already been validated and do not need rechecking. event_map: other events which have been fetched, in which we can - look up the redaaction events. Map from event id to event. + look up the redaction events. Map from event id to event. Returns: If the event should be redacted, a pruned event object. Otherwise, None. @@ -1695,7 +1717,12 @@ def _maybe_redact_event_row( # we choose to ignore redactions of m.room.create events. return None - for redaction_id in redactions: + for redaction_id in confirmed_redactions: + redacted_event = prune_event(original_ev) + redacted_event.internal_metadata.redacted_by = redaction_id + return redacted_event + + for redaction_id in unconfirmed_redactions: redaction_event = event_map.get(redaction_id) if not redaction_event or redaction_event.rejected_reason: # we don't have the redaction event, or the redaction event was not @@ -1736,12 +1763,10 @@ def _maybe_redact_event_row( # we found a good redaction event. Redact! redacted_event = prune_event(original_ev) - redacted_event.unsigned["redacted_by"] = redaction_id - - # It's fine to add the event directly, since get_pdu_json - # will serialise this field correctly - redacted_event.unsigned["redacted_because"] = redaction_event + redacted_event.internal_metadata.redacted_by = redaction_id + # Note: The `redacted_because` field will later be populated by + # `EventClientSerializer.serialize_event`. return redacted_event # no valid redaction found for this event diff --git a/synapse/storage/schema/__init__.py b/synapse/storage/schema/__init__.py index c4c4d7bcc4a..e3095a9d0d0 100644 --- a/synapse/storage/schema/__init__.py +++ b/synapse/storage/schema/__init__.py @@ -19,7 +19,7 @@ # # -SCHEMA_VERSION = 93 # remember to update the list below when updating +SCHEMA_VERSION = 94 # remember to update the list below when updating """Represents the expectations made by the codebase about the database schema This should be incremented whenever the codebase changes its requirements on the @@ -171,6 +171,9 @@ Changes in SCHEMA_VERSION = 93 - MSC4140: Set delayed events to be uniquely identifiable by their delay ID. + +Changes in SCHEMA_VERSION = 94 + - Add `recheck` column (boolean, default true) to the `redactions` table. """ diff --git a/synapse/storage/schema/main/delta/94/01_redactions_recheck.sql b/synapse/storage/schema/main/delta/94/01_redactions_recheck.sql new file mode 100644 index 00000000000..e99aa52f1f9 --- /dev/null +++ b/synapse/storage/schema/main/delta/94/01_redactions_recheck.sql @@ -0,0 +1,15 @@ +-- +-- This file is licensed under the Affero General Public License (AGPL) version 3. +-- +-- Copyright (C) 2026 Element Creations, Ltd +-- +-- This program is free software: you can redistribute it and/or modify +-- it under the terms of the GNU Affero General Public License as +-- published by the Free Software Foundation, either version 3 of the +-- License, or (at your option) any later version. +-- +-- See the GNU Affero General Public License for more details: +-- . + + +ALTER TABLE redactions ADD COLUMN recheck boolean NOT NULL DEFAULT true; diff --git a/synapse/storage/schema/main/delta/94/02_redactions_recheck_bg_update.sql b/synapse/storage/schema/main/delta/94/02_redactions_recheck_bg_update.sql new file mode 100644 index 00000000000..6367afd318e --- /dev/null +++ b/synapse/storage/schema/main/delta/94/02_redactions_recheck_bg_update.sql @@ -0,0 +1,15 @@ +-- +-- This file is licensed under the Affero General Public License (AGPL) version 3. +-- +-- Copyright (C) 2026 Element Creations, Ltd +-- +-- This program is free software: you can redistribute it and/or modify +-- it under the terms of the GNU Affero General Public License as +-- published by the Free Software Foundation, either version 3 of the +-- License, or (at your option) any later version. +-- +-- See the GNU Affero General Public License for more details: +-- . + +INSERT INTO background_updates (ordering, update_name, progress_json) VALUES + (9402, 'redactions_recheck', '{}'); diff --git a/synapse/synapse_rust/events.pyi b/synapse/synapse_rust/events.pyi index 185f29694b0..e6753bbbad2 100644 --- a/synapse/synapse_rust/events.pyi +++ b/synapse/synapse_rust/events.pyi @@ -21,6 +21,8 @@ class EventInternalMetadata: """the stream ordering of this event. None, until it has been persisted.""" instance_name: str | None """the instance name of the server that persisted this event. None, until it has been persisted.""" + redacted_by: str | None + """the event ID of the redaction event, if this event has been redacted. Set dynamically at load time, not persisted.""" outlier: bool """whether this event is an outlier (ie, whether we have the state at that diff --git a/synapse/types/storage/__init__.py b/synapse/types/storage/__init__.py index b01653246a9..992c36cabad 100644 --- a/synapse/types/storage/__init__.py +++ b/synapse/types/storage/__init__.py @@ -64,3 +64,5 @@ class _BackgroundUpdates: ) FIXUP_MAX_DEPTH_CAP = "fixup_max_depth_cap" + + REDACTIONS_RECHECK_BG_UPDATE = "redactions_recheck" diff --git a/tests/events/test_utils.py b/tests/events/test_utils.py index f511f577d31..af44b5dec1b 100644 --- a/tests/events/test_utils.py +++ b/tests/events/test_utils.py @@ -20,7 +20,7 @@ # import unittest as stdlib_unittest -from typing import Any, Mapping +from typing import TYPE_CHECKING, Any, Mapping from parameterized import parameterized @@ -37,11 +37,15 @@ make_config_for_admin, maybe_upsert_event_field, prune_event, - serialize_event, ) from synapse.types import JsonDict, create_requester from synapse.util.frozenutils import freeze +from tests.unittest import HomeserverTestCase + +if TYPE_CHECKING: + from synapse.server import HomeServer + def MockEvent(**kwargs: Any) -> EventBase: if "event_id" not in kwargs: @@ -638,19 +642,27 @@ def test_unsigned_is_copied(self) -> None: self.assertEqual(cloned.internal_metadata.txn_id, "txn") -class SerializeEventTestCase(stdlib_unittest.TestCase): +class SerializeEventTestCase(HomeserverTestCase): + def prepare(self, reactor: Any, clock: Any, hs: "HomeServer") -> None: + self._event_serializer = hs.get_event_client_serializer() + def serialize( self, ev: EventBase, fields: list[str] | None, include_admin_metadata: bool = False, + redaction_map: Mapping[str, EventBase] | None = None, ) -> JsonDict: - return serialize_event( - ev, - 1479807801915, - config=SerializeEventConfig( - only_event_fields=fields, include_admin_metadata=include_admin_metadata - ), + return self.get_success( + self._event_serializer.serialize_event( + ev, + 1479807801915, + config=SerializeEventConfig( + only_event_fields=fields, + include_admin_metadata=include_admin_metadata, + ), + redaction_map=redaction_map, + ) ) def test_event_fields_works_with_keys(self) -> None: @@ -764,9 +776,8 @@ def test_event_fields_all_fields_if_empty(self) -> None: def test_event_fields_fail_if_fields_not_str(self) -> None: with self.assertRaises(TypeError): - self.serialize( - MockEvent(room_id="!foo:bar", content={"foo": "bar"}), - ["room_id", 4], # type: ignore[list-item] + SerializeEventConfig( + only_event_fields=["room_id", 4], # type: ignore[list-item] ) def test_default_serialize_config_excludes_admin_metadata(self) -> None: @@ -867,6 +878,52 @@ def test_make_serialize_config_for_admin_retains_other_fields(self) -> None: ) self.assertTrue(admin_config.include_admin_metadata) + def test_redacted_because_is_filtered_out(self) -> None: + """If an event's unsigned dict has a `redacted_by` field, then the + `redacted_because` should be filtered out if not specified in + `only_event_fields`.""" + + redaction_id = "$redaction_event_id" + + event = MockEvent( + type="foo", + event_id="test", + room_id="!foo:bar", + content={"foo": "bar"}, + ) + event.internal_metadata.redacted_by = redaction_id + + redaction_event = MockEvent( + type="m.room.redaction", + event_id=redaction_id, + content={"redacts": "test"}, + ) + + self.assertEqual( + self.serialize( + event, + ["content.foo"], + redaction_map={redaction_id: redaction_event}, + ), + { + "content": {"foo": "bar"}, + }, + ) + + self.assertEqual( + self.serialize( + event, + ["content.foo", "unsigned.redacted_because"], + redaction_map={redaction_id: redaction_event}, + ), + { + "content": {"foo": "bar"}, + "unsigned": { + "redacted_because": self.serialize(redaction_event, fields=None), + }, + }, + ) + class CopyPowerLevelsContentTestCase(stdlib_unittest.TestCase): def setUp(self) -> None: diff --git a/tests/replication/storage/test_events.py b/tests/replication/storage/test_events.py index 28bfb8b8ea4..b7b94482ef0 100644 --- a/tests/replication/storage/test_events.py +++ b/tests/replication/storage/test_events.py @@ -101,11 +101,10 @@ def test_redactions(self) -> None: msg_dict = msg.get_dict() msg_dict["content"] = {} - msg_dict["unsigned"]["redacted_by"] = redaction.event_id - msg_dict["unsigned"]["redacted_because"] = redaction redacted = make_event_from_dict( msg_dict, internal_metadata_dict=msg.internal_metadata.get_dict() ) + redacted.internal_metadata.redacted_by = redaction.event_id self.check( "get_event", [msg.event_id], redacted, asserter=self.assertEventsEqual ) @@ -125,11 +124,10 @@ def test_backfilled_redactions(self) -> None: msg_dict = msg.get_dict() msg_dict["content"] = {} - msg_dict["unsigned"]["redacted_by"] = redaction.event_id - msg_dict["unsigned"]["redacted_because"] = redaction redacted = make_event_from_dict( msg_dict, internal_metadata_dict=msg.internal_metadata.get_dict() ) + redacted.internal_metadata.redacted_by = redaction.event_id self.check( "get_event", [msg.event_id], redacted, asserter=self.assertEventsEqual ) diff --git a/tests/rest/client/test_rooms.py b/tests/rest/client/test_rooms.py index f85c9939ce4..221121007da 100644 --- a/tests/rest/client/test_rooms.py +++ b/tests/rest/client/test_rooms.py @@ -4746,10 +4746,10 @@ def test_banning_remote_member_with_flag_redacts_their_events(self) -> None: original = self.get_success(self.store.get_event(message.event_id)) if not original: self.fail("Expected to find remote message in DB") - redacted_because = original.unsigned.get("redacted_because") - if not redacted_because: - self.fail("Did not find redacted_because field") - self.assertEqual(redacted_because.event_id, ban_event_id) + redacted_by = original.internal_metadata.redacted_by + if not redacted_by: + self.fail("Did not find redacted_by field") + self.assertEqual(redacted_by, ban_event_id) def test_unbanning_remote_user_stops_redaction_action(self) -> None: bad_user = "@remote_bad_user:" + self.OTHER_SERVER_NAME @@ -5111,7 +5111,7 @@ def test_kicking_remote_member_with_flag_redacts_their_events(self) -> None: original = self.get_success(self.store.get_event(message.event_id)) if not original: self.fail("Expected to find remote message in DB") - self.assertEqual(original.unsigned["redacted_by"], ban_event_id) + self.assertEqual(original.internal_metadata.redacted_by, ban_event_id) def test_rejoining_kicked_remote_user_stops_redaction_action(self) -> None: bad_user = "@remote_bad_user:" + self.OTHER_SERVER_NAME diff --git a/tests/storage/test_events_bg_updates.py b/tests/storage/test_events_bg_updates.py index d1a794c5a18..a5b53de77f2 100644 --- a/tests/storage/test_events_bg_updates.py +++ b/tests/storage/test_events_bg_updates.py @@ -14,11 +14,14 @@ # +from canonicaljson import encode_canonical_json + from twisted.internet.testing import MemoryReactor from synapse.api.constants import MAX_DEPTH from synapse.api.room_versions import RoomVersion, RoomVersions from synapse.server import HomeServer +from synapse.types.storage import _BackgroundUpdates from synapse.util.clock import Clock from tests.unittest import HomeserverTestCase @@ -154,3 +157,133 @@ def test_fixup_max_depth_cap_bg_update_old_room_version(self) -> None: # Assert that the topological_ordering of events has not been changed # from their depth. self.assertDictEqual(event_id_to_depth, dict(rows)) + + +class TestRedactionsRecheckBgUpdate(HomeserverTestCase): + """Test the background update that backfills the `recheck` column in redactions.""" + + def prepare( + self, reactor: MemoryReactor, clock: Clock, homeserver: HomeServer + ) -> None: + self.store = self.hs.get_datastores().main + self.db_pool = self.store.db_pool + + # Re-insert the background update, since it already ran during setup. + self.get_success( + self.db_pool.simple_insert( + table="background_updates", + values={ + "update_name": _BackgroundUpdates.REDACTIONS_RECHECK_BG_UPDATE, + "progress_json": "{}", + }, + ) + ) + self.db_pool.updates._all_done = False + + def _insert_redaction( + self, + event_id: str, + redacts: str, + recheck_redaction: bool | None = None, + insert_event_json: bool = True, + ) -> None: + """Insert a row into `redactions` and optionally a matching `event_json` row. + + Args: + event_id: The event ID of the redaction event. + redacts: The event ID being redacted. + recheck_redaction: The value of `recheck_redaction` in internal metadata. + If None, the key is omitted from internal metadata. + insert_event_json: Whether to insert a corresponding row in `event_json`. + """ + self.get_success( + self.db_pool.simple_insert( + table="redactions", + values={ + "event_id": event_id, + "redacts": redacts, + "have_censored": False, + "received_ts": 0, + }, + ) + ) + + if insert_event_json: + internal_metadata: dict = {} + if recheck_redaction is not None: + internal_metadata["recheck_redaction"] = recheck_redaction + + self.get_success( + self.db_pool.simple_insert( + table="event_json", + values={ + "event_id": event_id, + "room_id": "!room:test", + "internal_metadata": encode_canonical_json( + internal_metadata + ).decode("utf-8"), + "json": "{}", + "format_version": 3, + }, + ) + ) + + def _get_recheck(self, event_id: str) -> bool: + row = self.get_success( + self.db_pool.simple_select_one( + table="redactions", + keyvalues={"event_id": event_id}, + retcols=["recheck"], + ) + ) + return bool(row[0]) + + def test_recheck_true(self) -> None: + """A redaction with recheck_redaction=True in internal metadata gets recheck=True.""" + self._insert_redaction("$redact1:test", "$target1:test", recheck_redaction=True) + + self.wait_for_background_updates() + + self.assertTrue(self._get_recheck("$redact1:test")) + + def test_recheck_false(self) -> None: + """A redaction with recheck_redaction=False in internal metadata gets recheck=False.""" + self._insert_redaction( + "$redact2:test", "$target2:test", recheck_redaction=False + ) + + self.wait_for_background_updates() + + self.assertFalse(self._get_recheck("$redact2:test")) + + def test_recheck_absent_from_metadata(self) -> None: + """A redaction with no recheck_redaction key in internal metadata gets recheck=False.""" + self._insert_redaction("$redact3:test", "$target3:test", recheck_redaction=None) + + self.wait_for_background_updates() + + self.assertFalse(self._get_recheck("$redact3:test")) + + def test_recheck_no_event_json(self) -> None: + """A redaction with no event_json row gets recheck=False.""" + self._insert_redaction( + "$redact4:test", "$target4:test", insert_event_json=False + ) + + self.wait_for_background_updates() + + self.assertFalse(self._get_recheck("$redact4:test")) + + def test_batching(self) -> None: + """The update processes rows in batches, completing when all are done.""" + self._insert_redaction("$redact5:test", "$target5:test", recheck_redaction=True) + self._insert_redaction( + "$redact6:test", "$target6:test", recheck_redaction=False + ) + self._insert_redaction("$redact7:test", "$target7:test", recheck_redaction=True) + + self.wait_for_background_updates() + + self.assertTrue(self._get_recheck("$redact5:test")) + self.assertFalse(self._get_recheck("$redact6:test")) + self.assertTrue(self._get_recheck("$redact7:test")) diff --git a/tests/storage/test_redaction.py b/tests/storage/test_redaction.py index 92eb99f1d50..c82ccf1600b 100644 --- a/tests/storage/test_redaction.py +++ b/tests/storage/test_redaction.py @@ -158,7 +158,7 @@ def test_redact(self) -> None: event, ) - self.assertFalse("redacted_because" in event.unsigned) + self.assertIsNone(event.internal_metadata.redacted_by) # Redact event reason = "Because I said so" @@ -168,7 +168,7 @@ def test_redact(self) -> None: self.assertEqual(msg_event.event_id, event.event_id) - self.assertTrue("redacted_because" in event.unsigned) + self.assertIsNotNone(event.internal_metadata.redacted_by) self.assertObjectHasAttributes( { @@ -179,15 +179,6 @@ def test_redact(self) -> None: event, ) - self.assertObjectHasAttributes( - { - "type": EventTypes.Redaction, - "user_id": self.u_alice.to_string(), - "content": {"reason": reason}, - }, - event.unsigned["redacted_because"], - ) - def test_redact_join(self) -> None: self.inject_room_member(self.room1, self.u_alice, Membership.JOIN) @@ -206,7 +197,7 @@ def test_redact_join(self) -> None: event, ) - self.assertFalse(hasattr(event, "redacted_because")) + self.assertIsNone(event.internal_metadata.redacted_by) # Redact event reason = "Because I said so" @@ -216,7 +207,7 @@ def test_redact_join(self) -> None: event = self.get_success(self.store.get_event(msg_event.event_id)) - self.assertTrue("redacted_because" in event.unsigned) + self.assertIsNotNone(event.internal_metadata.redacted_by) self.assertObjectHasAttributes( { @@ -227,15 +218,6 @@ def test_redact_join(self) -> None: event, ) - self.assertObjectHasAttributes( - { - "type": EventTypes.Redaction, - "user_id": self.u_alice.to_string(), - "content": {"reason": reason}, - }, - event.unsigned["redacted_because"], - ) - def test_circular_redaction(self) -> None: redaction_event_id1 = "$redaction1_id:test" redaction_event_id2 = "$redaction2_id:test" @@ -331,10 +313,7 @@ def internal_metadata(self) -> EventInternalMetadata: fetched = self.get_success(self.store.get_event(redaction_event_id1)) # it should have been redacted - self.assertEqual(fetched.unsigned["redacted_by"], redaction_event_id2) - self.assertEqual( - fetched.unsigned["redacted_because"].event_id, redaction_event_id2 - ) + self.assertEqual(fetched.internal_metadata.redacted_by, redaction_event_id2) def test_redact_censor(self) -> None: """Test that a redacted event gets censored in the DB after a month""" @@ -355,7 +334,7 @@ def test_redact_censor(self) -> None: event, ) - self.assertFalse("redacted_because" in event.unsigned) + self.assertIsNone(event.internal_metadata.redacted_by) # Redact event reason = "Because I said so" @@ -363,7 +342,7 @@ def test_redact_censor(self) -> None: event = self.get_success(self.store.get_event(msg_event.event_id)) - self.assertTrue("redacted_because" in event.unsigned) + self.assertIsNotNone(event.internal_metadata.redacted_by) self.assertObjectHasAttributes( { From 0549307198cc515e5bb292a56b9d0b5edd616e7c Mon Sep 17 00:00:00 2001 From: Erik Johnston Date: Fri, 27 Mar 2026 10:53:16 +0000 Subject: [PATCH 103/121] Revert "Limit outgoing to_device EDU size to 65536" (#19614) Reverts element-hq/synapse#18416 Unfortunately, this causes failures on `/sendToDevice` endpoint in normal circumstances. If a single user has, say, a hundred devices then we easily go over the limit. This blocks message sending entirely in encrypted rooms. cc @MadLittleMods @MatMaul --- changelog.d/18416.bugfix | 1 - synapse/api/constants.py | 16 -- .../sender/per_destination_queue.py | 13 +- synapse/handlers/devicemessage.py | 158 +++-------------- synapse/storage/databases/main/deviceinbox.py | 47 +----- tests/handlers/test_appservice.py | 4 +- tests/rest/client/test_sendtodevice.py | 159 +----------------- 7 files changed, 38 insertions(+), 360 deletions(-) delete mode 100644 changelog.d/18416.bugfix diff --git a/changelog.d/18416.bugfix b/changelog.d/18416.bugfix deleted file mode 100644 index 71f181fed40..00000000000 --- a/changelog.d/18416.bugfix +++ /dev/null @@ -1 +0,0 @@ -A long queue of to-device messages could prevent outgoing federation because of the size of the transaction, let's limit the to-device EDU size to a reasonable value. diff --git a/synapse/api/constants.py b/synapse/api/constants.py index 27f06ec525b..eb9e6cc39b9 100644 --- a/synapse/api/constants.py +++ b/synapse/api/constants.py @@ -30,22 +30,6 @@ # the max size of a (canonical-json-encoded) event MAX_PDU_SIZE = 65536 -# This isn't spec'ed but is our own reasonable default to play nice with Synapse's -# `max_request_size`/`max_request_body_size`. We chose the same as `MAX_PDU_SIZE` as our -# `max_request_body_size` math is currently limited by 200 `MAX_PDU_SIZE` things. The -# spec for a `/federation/v1/send` request sets the limit at 100 EDU's and 50 PDU's -# which is below that 200 `MAX_PDU_SIZE` limit (`max_request_body_size`). -# -# Allowing oversized EDU's results in failed `/federation/v1/send` transactions (because -# the request overall can overrun the `max_request_body_size`) which are retried over -# and over and prevent other outbound federation traffic from happening. -MAX_EDU_SIZE = 65536 - -# This is defined in the Matrix spec and enforced by the receiver. -MAX_EDUS_PER_TRANSACTION = 100 -# A transaction can contain up to 100 EDUs but synapse reserves 10 EDUs for other purposes -# like trickling out some device list updates. -NUMBER_OF_RESERVED_EDUS_PER_TRANSACTION = 10 # The maximum allowed size of an HTTP request. # Other than media uploads, the biggest request we expect to see is a fully-loaded diff --git a/synapse/federation/sender/per_destination_queue.py b/synapse/federation/sender/per_destination_queue.py index 32f8630c9d1..cdacf16d725 100644 --- a/synapse/federation/sender/per_destination_queue.py +++ b/synapse/federation/sender/per_destination_queue.py @@ -30,11 +30,7 @@ from twisted.internet import defer -from synapse.api.constants import ( - MAX_EDUS_PER_TRANSACTION, - NUMBER_OF_RESERVED_EDUS_PER_TRANSACTION, - EduTypes, -) +from synapse.api.constants import EduTypes from synapse.api.errors import ( FederationDeniedError, HttpResponseException, @@ -55,6 +51,9 @@ if TYPE_CHECKING: import synapse.server +# This is defined in the Matrix spec and enforced by the receiver. +MAX_EDUS_PER_TRANSACTION = 100 + logger = logging.getLogger(__name__) @@ -799,9 +798,7 @@ async def __aenter__(self) -> tuple[list[EventBase], list[Edu]]: ( to_device_edus, device_stream_id, - ) = await self.queue._get_to_device_message_edus( - edu_limit - NUMBER_OF_RESERVED_EDUS_PER_TRANSACTION - ) + ) = await self.queue._get_to_device_message_edus(edu_limit - 10) if to_device_edus: self._device_stream_id = device_stream_id diff --git a/synapse/handlers/devicemessage.py b/synapse/handlers/devicemessage.py index 2096b44f6ca..0ef14b31dad 100644 --- a/synapse/handlers/devicemessage.py +++ b/synapse/handlers/devicemessage.py @@ -23,15 +23,8 @@ from http import HTTPStatus from typing import TYPE_CHECKING, Any -from canonicaljson import encode_canonical_json - -from synapse.api.constants import ( - MAX_EDU_SIZE, - EduTypes, - EventContentFields, - ToDeviceEventTypes, -) -from synapse.api.errors import Codes, EventSizeError, SynapseError +from synapse.api.constants import EduTypes, EventContentFields, ToDeviceEventTypes +from synapse.api.errors import Codes, SynapseError from synapse.api.ratelimiting import Ratelimiter from synapse.logging.context import run_in_background from synapse.logging.opentracing import ( @@ -42,7 +35,7 @@ ) from synapse.types import JsonDict, Requester, StreamKeyType, UserID, get_domain_from_id from synapse.util.json import json_encoder -from synapse.util.stringutils import random_string_insecure_fast +from synapse.util.stringutils import random_string if TYPE_CHECKING: from synapse.server import HomeServer @@ -229,7 +222,6 @@ async def send_device_message( set_tag(SynapseTags.TO_DEVICE_TYPE, message_type) set_tag(SynapseTags.TO_DEVICE_SENDER, sender_user_id) local_messages = {} - # Map from destination (server) -> recipient (user ID) -> device_id -> JSON message content remote_messages: dict[str, dict[str, dict[str, JsonDict]]] = {} for user_id, by_device in messages.items(): if not UserID.is_valid(user_id): @@ -285,33 +277,28 @@ async def send_device_message( destination = get_domain_from_id(user_id) remote_messages.setdefault(destination, {})[user_id] = by_device - # Add local messages to the database. + context = get_active_span_text_map() + + remote_edu_contents = {} + for destination, messages in remote_messages.items(): + # The EDU contains a "message_id" property which is used for + # idempotence. Make up a random one. + message_id = random_string(16) + log_kv({"destination": destination, "message_id": message_id}) + + remote_edu_contents[destination] = { + "messages": messages, + "sender": sender_user_id, + "type": message_type, + "message_id": message_id, + "org.matrix.opentracing_context": json_encoder.encode(context), + } + + # Add messages to the database. # Retrieve the stream id of the last-processed to-device message. - last_stream_id = ( - await self.store.add_local_messages_from_client_to_device_inbox( - local_messages - ) + last_stream_id = await self.store.add_messages_to_device_inbox( + local_messages, remote_edu_contents ) - for destination, messages in remote_messages.items(): - split_edus = split_device_messages_into_edus( - sender_user_id, message_type, messages - ) - for edu in split_edus: - edu["org.matrix.opentracing_context"] = json_encoder.encode( - get_active_span_text_map() - ) - # Add remote messages to the database. - last_stream_id = ( - await self.store.add_remote_messages_from_client_to_device_inbox( - {destination: edu} - ) - ) - log_kv( - { - "destination": destination, - "message_id": edu["message_id"], - } - ) # Notify listeners that there are new to-device messages to process, # handing them the latest stream id. @@ -410,102 +397,3 @@ async def get_events_for_dehydrated_device( "events": messages, "next_batch": f"d{stream_id}", } - - -def split_device_messages_into_edus( - sender_user_id: str, - message_type: str, - messages_by_user_then_device: dict[str, dict[str, JsonDict]], -) -> list[JsonDict]: - """ - This function takes many to-device messages and fits/splits them into several EDUs - as necessary. We split the messages up as the overall request can overrun the - `max_request_body_size` and prevent outbound federation traffic because of the size - of the transaction (cf. `MAX_EDU_SIZE`). - - Args: - sender_user_id: The user that is sending the to-device messages. - message_type: The type of to-device messages that are being sent. - messages_by_user_then_device: Dictionary of recipient user_id to recipient device_id to message. - - Returns: - Bin-packed list of EDU JSON content for the given to_device messages - - Raises: - EventSizeError: If a single to-device message is too large to fit into an EDU. - """ - split_edus_content: list[JsonDict] = [] - - # Convert messages dict to a list of (recipient, messages_by_device) pairs - message_items = list(messages_by_user_then_device.items()) - - while message_items: - edu_messages = {} - # Start by trying to fit all remaining messages - target_count = len(message_items) - - while target_count > 0: - # Take the first target_count messages - edu_messages = dict(message_items[:target_count]) - edu_content = create_new_to_device_edu_content( - sender_user_id, message_type, edu_messages - ) - # Let's add the whole EDU structure before testing the size - edu = { - "content": edu_content, - "edu_type": EduTypes.DIRECT_TO_DEVICE, - } - - if len(encode_canonical_json(edu)) <= MAX_EDU_SIZE: - # It fits! Add this EDU and remove these messages from the list - split_edus_content.append(edu_content) - message_items = message_items[target_count:] - - logger.debug( - "Created EDU with %d recipients from %s (message_id=%s), (total EDUs so far: %d)", - target_count, - sender_user_id, - edu_content["message_id"], - len(split_edus_content), - ) - break - else: - if target_count == 1: - # Single recipient's messages are too large, let's reject the client - # call with 413/`M_TOO_LARGE`, we expect this error to reach the - # client in the case of the /sendToDevice endpoint. - # - # 413 is currently an unspecced response for `/sendToDevice` but is - # probably the best thing we can do. - # https://github.com/matrix-org/matrix-spec/pull/2340 tracks adding - # this to the spec - recipient = message_items[0][0] - raise EventSizeError( - f"device message to {recipient} too large to fit in a single EDU", - unpersistable=True, - ) - - # Halve the number of messages and try again - target_count = target_count // 2 - - return split_edus_content - - -def create_new_to_device_edu_content( - sender_user_id: str, - message_type: str, - messages_by_user_then_device: dict[str, dict[str, JsonDict]], -) -> JsonDict: - """ - Create a new `m.direct_to_device` EDU `content` object with a unique message ID. - """ - # The EDU contains a "message_id" property which is used for - # idempotence. Make up a random one. - message_id = random_string_insecure_fast(16) - content = { - "sender": sender_user_id, - "type": message_type, - "message_id": message_id, - "messages": messages_by_user_then_device, - } - return content diff --git a/synapse/storage/databases/main/deviceinbox.py b/synapse/storage/databases/main/deviceinbox.py index 9fed22c1b3f..fc61f46c1c5 100644 --- a/synapse/storage/databases/main/deviceinbox.py +++ b/synapse/storage/databases/main/deviceinbox.py @@ -739,15 +739,18 @@ def get_all_new_device_messages_txn( ) @trace - async def add_local_messages_from_client_to_device_inbox( + async def add_messages_to_device_inbox( self, local_messages_by_user_then_device: dict[str, dict[str, JsonDict]], + remote_messages_by_destination: dict[str, JsonDict], ) -> int: - """Queue local device messages that will be sent to devices of local users. + """Used to send messages from this server. Args: local_messages_by_user_then_device: Dictionary of recipient user_id to recipient device_id to message. + remote_messages_by_destination: + Dictionary of destination server_name to the EDU JSON to send. Returns: The new stream_id. @@ -763,39 +766,6 @@ def add_messages_txn( txn, stream_id, local_messages_by_user_then_device ) - async with self._to_device_msg_id_gen.get_next() as stream_id: - now_ms = self.clock.time_msec() - await self.db_pool.runInteraction( - "add_local_messages_from_client_to_device_inbox", - add_messages_txn, - now_ms, - stream_id, - ) - for user_id in local_messages_by_user_then_device.keys(): - self._device_inbox_stream_cache.entity_has_changed(user_id, stream_id) - - return self._to_device_msg_id_gen.get_current_token() - - @trace - async def add_remote_messages_from_client_to_device_inbox( - self, - remote_messages_by_destination: dict[str, JsonDict], - ) -> int: - """Queue device messages that will be sent to remote servers. - - Args: - remote_messages_by_destination: - Dictionary of destination server_name to the EDU JSON to send. - - Returns: - The new stream_id. - """ - - assert self._can_write_to_device - - def add_messages_txn( - txn: LoggingTransaction, now_ms: int, stream_id: int - ) -> None: # Add the remote messages to the federation outbox. # We'll send them to a remote server when we next send a # federation transaction to that destination. @@ -854,11 +824,10 @@ def add_messages_txn( async with self._to_device_msg_id_gen.get_next() as stream_id: now_ms = self.clock.time_msec() await self.db_pool.runInteraction( - "add_remote_messages_from_client_to_device_inbox", - add_messages_txn, - now_ms, - stream_id, + "add_messages_to_device_inbox", add_messages_txn, now_ms, stream_id ) + for user_id in local_messages_by_user_then_device.keys(): + self._device_inbox_stream_cache.entity_has_changed(user_id, stream_id) for destination in remote_messages_by_destination.keys(): self._device_federation_outbox_stream_cache.entity_has_changed( destination, stream_id diff --git a/tests/handlers/test_appservice.py b/tests/handlers/test_appservice.py index ac7411291bc..6336edb108a 100644 --- a/tests/handlers/test_appservice.py +++ b/tests/handlers/test_appservice.py @@ -856,9 +856,7 @@ def test_application_services_receive_bursts_of_to_device(self) -> None: # Seed the device_inbox table with our fake messages self.get_success( - self.hs.get_datastores().main.add_local_messages_from_client_to_device_inbox( - messages - ) + self.hs.get_datastores().main.add_messages_to_device_inbox(messages, {}) ) # Now have local_user send a final to-device message to exclusive_as_user. All unsent diff --git a/tests/rest/client/test_sendtodevice.py b/tests/rest/client/test_sendtodevice.py index 526d9f0b3c4..56533d85f58 100644 --- a/tests/rest/client/test_sendtodevice.py +++ b/tests/rest/client/test_sendtodevice.py @@ -18,21 +18,9 @@ # [This file includes modifications made by New Vector Limited] # # -from unittest.mock import AsyncMock, Mock - -from twisted.test.proto_helpers import MemoryReactor - -from synapse.api.constants import ( - MAX_EDU_SIZE, - MAX_EDUS_PER_TRANSACTION, - EduTypes, -) -from synapse.api.errors import Codes +from synapse.api.constants import EduTypes from synapse.rest import admin from synapse.rest.client import login, sendtodevice, sync -from synapse.server import HomeServer -from synapse.types import JsonDict -from synapse.util.clock import Clock from tests.unittest import HomeserverTestCase, override_config @@ -53,20 +41,6 @@ class SendToDeviceTestCase(HomeserverTestCase): sync.register_servlets, ] - def default_config(self) -> JsonDict: - config = super().default_config() - config["federation_sender_instances"] = None - return config - - def make_homeserver(self, reactor: MemoryReactor, clock: Clock) -> HomeServer: - self.federation_transport_client = Mock(spec=["send_transaction"]) - self.federation_transport_client.send_transaction = AsyncMock() - hs = self.setup_test_homeserver( - federation_transport_client=self.federation_transport_client, - ) - - return hs - def test_user_to_user(self) -> None: """A to-device message from one user to another should get delivered""" @@ -117,137 +91,6 @@ def test_user_to_user(self) -> None: self.assertEqual(channel.code, 200, channel.result) self.assertEqual(channel.json_body.get("to_device", {}).get("events", []), []) - def test_large_remote_todevice(self) -> None: - """A to-device message needs to fit in the EDU size limit""" - _ = self.register_user("u1", "pass") - user1_tok = self.login("u1", "pass", "d1") - - # Create a message that is over the `MAX_EDU_SIZE` - test_msg = {"foo": "a" * MAX_EDU_SIZE} - channel = self.make_request( - "PUT", - "/_matrix/client/r0/sendToDevice/m.test/12345", - content={"messages": {"@remote_user:secondserver": {"device": test_msg}}}, - access_token=user1_tok, - ) - self.assertEqual(channel.code, 413, channel.result) - self.assertEqual(Codes.TOO_LARGE, channel.json_body["errcode"]) - - def test_edu_large_messages_splitting(self) -> None: - """ - Test that a bunch of to-device messages are split over multiple EDUs if they are - collectively too large to fit into a single EDU - """ - mock_send_transaction: AsyncMock = ( - self.federation_transport_client.send_transaction - ) - mock_send_transaction.return_value = {} - - sender = self.hs.get_federation_sender() - - _ = self.register_user("u1", "pass") - user1_tok = self.login("u1", "pass", "d1") - destination = "secondserver" - messages = {} - - # 2 messages, each just big enough to fit into their own EDU - for i in range(2): - messages[f"@remote_user{i}:" + destination] = { - "device": {"foo": "a" * (MAX_EDU_SIZE - 1000)} - } - - channel = self.make_request( - "PUT", - "/_matrix/client/r0/sendToDevice/m.test/1234567", - content={"messages": messages}, - access_token=user1_tok, - ) - self.assertEqual(channel.code, 200, channel.result) - - self.get_success(sender.send_device_messages([destination])) - - number_of_edus_sent = 0 - for call in mock_send_transaction.call_args_list: - number_of_edus_sent += len(call[0][1]()["edus"]) - - self.assertEqual(number_of_edus_sent, 2) - - def test_edu_small_messages_not_splitting(self) -> None: - """ - Test that a couple of small messages do not get split into multiple EDUs - """ - mock_send_transaction: AsyncMock = ( - self.federation_transport_client.send_transaction - ) - mock_send_transaction.return_value = {} - - sender = self.hs.get_federation_sender() - - _ = self.register_user("u1", "pass") - user1_tok = self.login("u1", "pass", "d1") - destination = "secondserver" - messages = {} - - # 2 small messages that should fit in a single EDU - for i in range(2): - messages[f"@remote_user{i}:" + destination] = {"device": {"foo": "a" * 100}} - - channel = self.make_request( - "PUT", - "/_matrix/client/r0/sendToDevice/m.test/123456", - content={"messages": messages}, - access_token=user1_tok, - ) - self.assertEqual(channel.code, 200, channel.result) - - self.get_success(sender.send_device_messages([destination])) - - number_of_edus_sent = 0 - for call in mock_send_transaction.call_args_list: - number_of_edus_sent += len(call[0][1]()["edus"]) - - self.assertEqual(number_of_edus_sent, 1) - - def test_transaction_splitting(self) -> None: - """Test that a bunch of to-device messages are split into multiple transactions if there are too many EDUs""" - mock_send_transaction: AsyncMock = ( - self.federation_transport_client.send_transaction - ) - mock_send_transaction.return_value = {} - - sender = self.hs.get_federation_sender() - - _ = self.register_user("u1", "pass") - user1_tok = self.login("u1", "pass", "d1") - destination = "secondserver" - messages = {} - - number_of_edus_to_send = MAX_EDUS_PER_TRANSACTION + 1 - - for i in range(number_of_edus_to_send): - messages[f"@remote_user{i}:" + destination] = { - "device": {"foo": "a" * (MAX_EDU_SIZE - 1000)} - } - - channel = self.make_request( - "PUT", - "/_matrix/client/r0/sendToDevice/m.test/12345678", - content={"messages": messages}, - access_token=user1_tok, - ) - self.assertEqual(channel.code, 200, channel.result) - - self.get_success(sender.send_device_messages([destination])) - - # At least 2 transactions should be sent since we are over the EDU limit per transaction - self.assertGreaterEqual(mock_send_transaction.call_count, 2) - - number_of_edus_sent = 0 - for call in mock_send_transaction.call_args_list: - number_of_edus_sent += len(call[0][1]()["edus"]) - - self.assertEqual(number_of_edus_sent, number_of_edus_to_send) - @override_config({"rc_key_requests": {"per_second": 10, "burst_count": 2}}) def test_local_room_key_request(self) -> None: """m.room_key_request has special-casing; test from local user""" From f772fad1cd7ab86e76fe86481792214e33270e78 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20Gonz=C3=A1lez?= Date: Mon, 30 Mar 2026 18:14:25 +0200 Subject: [PATCH 104/121] Fix small comment typo in config output from the `demo/start.sh` script (#19538) --- changelog.d/19538.misc | 1 + demo/start.sh | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) create mode 100644 changelog.d/19538.misc diff --git a/changelog.d/19538.misc b/changelog.d/19538.misc new file mode 100644 index 00000000000..56d3d2744d0 --- /dev/null +++ b/changelog.d/19538.misc @@ -0,0 +1 @@ +Fix small comment typo in config output from the `demo/start.sh` script. diff --git a/demo/start.sh b/demo/start.sh index 0b61ac99910..471e881f461 100755 --- a/demo/start.sh +++ b/demo/start.sh @@ -49,7 +49,7 @@ for port in 8080 8081 8082; do # Please don't accidentally bork me with your fancy settings. listeners=$(cat <<-PORTLISTENERS # Configure server to listen on both $https_port and $port - # This overides some of the default settings above + # This overrides some of the default settings above listeners: - port: $https_port type: http From 63c7f03edc5bc50aed5b6f57074a435d792fb79a Mon Sep 17 00:00:00 2001 From: Jason Little Date: Mon, 30 Mar 2026 12:31:22 -0500 Subject: [PATCH 105/121] update poetry lock --- poetry.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/poetry.lock b/poetry.lock index d4d35d699e5..a2e0eeafa84 100644 --- a/poetry.lock +++ b/poetry.lock @@ -3806,4 +3806,4 @@ url-preview = ["lxml"] [metadata] lock-version = "2.1" python-versions = ">=3.10.0,<4.0.0" -content-hash = "ce9ac9da9e7ffaf24b3e1e7892342ba486e7af4ea25385f875d0f3a2d5c5d133" +content-hash = "515c310623717d6da6b6ab8ffc9aac9195ffdd8719826b371714431baea0b475" From 70c6796b98a06adca969354b935589dc6664db7f Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Mon, 30 Mar 2026 15:11:36 -0600 Subject: [PATCH 106/121] Increase timeout for policy server requests (#19629) This is to accommodate media scanning and checking. Currently, a 3s timeout means we make 2-5 requests before a media item is successfully scanned. Number chosen based on vibes and light real world testing. Fixes https://github.com/matrix-org/policyserv/issues/108 ### Pull Request Checklist * [x] Pull request is based on the develop branch * [x] Pull request includes a [changelog file](https://element-hq.github.io/synapse/latest/development/contributing_guide.html#changelog). The entry should: - Be a short description of your change which makes sense to users. "Fixed a bug that prevented receiving messages from other servers." instead of "Moved X method from `EventStore` to `EventWorkerStore`.". - Use markdown where necessary, mostly for `code blocks`. - End with either a period (.) or an exclamation mark (!). - Start with a capital letter. - Feel free to credit yourself, by adding a sentence "Contributed by @github_username." or "Contributed by [Your Name]." to the end of the entry. * [x] [Code style](https://element-hq.github.io/synapse/latest/code_style.html) is correct (run the [linters](https://element-hq.github.io/synapse/latest/development/contributing_guide.html#run-the-linters)) --- changelog.d/19629.bugfix | 1 + synapse/handlers/room_policy.py | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) create mode 100644 changelog.d/19629.bugfix diff --git a/changelog.d/19629.bugfix b/changelog.d/19629.bugfix new file mode 100644 index 00000000000..06a03d78199 --- /dev/null +++ b/changelog.d/19629.bugfix @@ -0,0 +1 @@ +Increase timeout for policy server requests to avoid repeated requests for checking media. \ No newline at end of file diff --git a/synapse/handlers/room_policy.py b/synapse/handlers/room_policy.py index 57163c43444..fee3c6cfafb 100644 --- a/synapse/handlers/room_policy.py +++ b/synapse/handlers/room_policy.py @@ -228,7 +228,7 @@ async def ask_policy_server_to_sign_event( signature = await self._federation_client.ask_policy_server_to_sign_event( policy_server.server_name, event, - timeout=3000, + timeout=30000, ) # TODO: We can *probably* remove this when we remove unstable MSC4284 support. # The server *should* be returning either a signature or an error, but there could From 582f976d40733315b0b98a02eec75a5aa48162c4 Mon Sep 17 00:00:00 2001 From: Cadence Ember Date: Tue, 31 Mar 2026 15:36:16 +1300 Subject: [PATCH 107/121] Unprefix room_version & encryption from hierarchy API (#19576) These seem to work properly in the hierarchy API so I figured I'd unprefix them and remove the flag requirement. This should [better align with the spec](https://spec.matrix.org/latest/client-server-api/#get_matrixclientv1roomsroomidhierarchy_response-200_spacehierarchyroomschunk) and Matrix v1.15 (via [MSC3266](https://github.com/matrix-org/matrix-spec-proposals/pull/3266)), which says these properties are available and named like this. Related to #18731. This doesn't affect the stability or experimental flags of the room summary API, just the hierarchy API. --- changelog.d/19576.feature | 1 + synapse/handlers/room_summary.py | 7 ++----- 2 files changed, 3 insertions(+), 5 deletions(-) create mode 100644 changelog.d/19576.feature diff --git a/changelog.d/19576.feature b/changelog.d/19576.feature new file mode 100644 index 00000000000..e346c717207 --- /dev/null +++ b/changelog.d/19576.feature @@ -0,0 +1 @@ +Stabilize `room_version` and `encryption` fields in the space/room `/hierarchy` API (part of [MSC3266](https://github.com/matrix-org/matrix-spec-proposals/pull/3266)). \ No newline at end of file diff --git a/synapse/handlers/room_summary.py b/synapse/handlers/room_summary.py index 9ec0d33f113..bbcdc0877e9 100644 --- a/synapse/handlers/room_summary.py +++ b/synapse/handlers/room_summary.py @@ -128,7 +128,6 @@ def __init__(self, hs: "HomeServer"): name="get_room_hierarchy", server_name=self.server_name, ) - self._msc3266_enabled = hs.config.experimental.msc3266_enabled async def get_room_hierarchy( self, @@ -791,6 +790,7 @@ async def _build_room_entry(self, room_id: str, for_federation: bool) -> JsonDic entry: JsonDict = { "room_id": stats.room_id, + "room_version": stats.version, "name": stats.name, "topic": stats.topic, "canonical_alias": stats.canonical_alias, @@ -802,12 +802,9 @@ async def _build_room_entry(self, room_id: str, for_federation: bool) -> JsonDic ), "guest_can_join": stats.guest_access == "can_join", "room_type": stats.room_type, + "encryption": stats.encryption, } - if self._msc3266_enabled: - entry["im.nheko.summary.version"] = stats.version - entry["im.nheko.summary.encryption"] = stats.encryption - # Federation requests need to provide additional information so the # requested server is able to filter the response appropriately. if for_federation: From 4fc1b92853bb0b68de5c50bac06a54dcacdb73c6 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 31 Mar 2026 11:47:08 +0200 Subject: [PATCH 108/121] Bump actions/download-artifact from 7.0.0 to 8.0.1 (#19626) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps [actions/download-artifact](https://github.com/actions/download-artifact) from 7.0.0 to 8.0.1.
Release notes

Sourced from actions/download-artifact's releases.

v8.0.1

What's Changed

Full Changelog: https://github.com/actions/download-artifact/compare/v8...v8.0.1

v8.0.0

v8 - What's new

[!IMPORTANT] actions/download-artifact@v8 has been migrated to an ESM module. This should be transparent to the caller but forks might need to make significant changes.

[!IMPORTANT] Hash mismatches will now error by default. Users can override this behavior with a setting change (see below).

Direct downloads

To support direct uploads in actions/upload-artifact, the action will no longer attempt to unzip all downloaded files. Instead, the action checks the Content-Type header ahead of unzipping and skips non-zipped files. Callers wishing to download a zipped file as-is can also set the new skip-decompress parameter to true.

Enforced checks (breaking)

A previous release introduced digest checks on the download. If a download hash didn't match the expected hash from the server, the action would log a warning. Callers can now configure the behavior on mismatch with the digest-mismatch parameter. To be secure by default, we are now defaulting the behavior to error which will fail the workflow run.

ESM

To support new versions of the @actions/* packages, we've upgraded the package to ESM.

What's Changed

Full Changelog: https://github.com/actions/download-artifact/compare/v7...v8.0.0

Commits
  • 3e5f45b Add regression tests for CJK characters (#471)
  • e6d03f6 Add a regression test for artifact name + content-type mismatches (#472)
  • 70fc10c Merge pull request #461 from actions/danwkennedy/digest-mismatch-behavior
  • f258da9 Add change docs
  • ccc058e Fix linting issues
  • bd7976b Add a setting to specify what to do on hash mismatch and default it to error
  • ac21fcf Merge pull request #460 from actions/danwkennedy/download-no-unzip
  • 15999bf Add note about package bumps
  • 974686e Bump the version to v8 and add release notes
  • fbe48b1 Update test names to make it clearer what they do
  • Additional commits viewable in compare view

[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=actions/download-artifact&package-manager=github_actions&previous-version=7.0.0&new-version=8.0.1)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores) Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`. [//]: # (dependabot-automerge-start) [//]: # (dependabot-automerge-end) ---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot show ignore conditions` will show all of the ignore conditions of the specified dependency - `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself)
Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/docker.yml | 2 +- .github/workflows/release-artifacts.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml index d581b5ec566..da9cd02c78f 100644 --- a/.github/workflows/docker.yml +++ b/.github/workflows/docker.yml @@ -129,7 +129,7 @@ jobs: - build steps: - name: Download digests - uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0 + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 with: path: ${{ runner.temp }}/digests pattern: digests-* diff --git a/.github/workflows/release-artifacts.yml b/.github/workflows/release-artifacts.yml index 95d3d98d75b..c313a64250f 100644 --- a/.github/workflows/release-artifacts.yml +++ b/.github/workflows/release-artifacts.yml @@ -187,7 +187,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Download all workflow run artifacts - uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0 + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 - name: Build a tarball for the debs # We need to merge all the debs uploads into one folder, then compress # that. From 90449b915d17131e42d1b3963389c9a05cadc813 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 31 Mar 2026 11:47:55 +0200 Subject: [PATCH 109/121] Bump the minor-and-patches group with 2 updates (#19624) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps the minor-and-patches group with 2 updates: [tailscale/github-action](https://github.com/tailscale/github-action) and [Swatinem/rust-cache](https://github.com/swatinem/rust-cache). Updates `tailscale/github-action` from 4.1.1 to 4.1.2
Release notes

Sourced from tailscale/github-action's releases.

v4.1.2

What's Changed

New Contributors

Full Changelog: https://github.com/tailscale/github-action/compare/v4.1.1...v4.1.2

Commits
  • 306e68a Bump @​actions/cache from 5.0.1 to 5.0.2
  • 989d9be Bump @​actions/tool-cache from 2.0.2 to 3.0.0
  • 69584d7 Bump @​actions/github from 6.0.1 to 7.0.0
  • cda17d5 Bump @​actions/core from 2.0.1 to 2.0.2
  • 81231eb Bump actions/setup-node from 6.2.0 to 6.3.0
  • 48c6d03 action,dist,src: bump default version to 1.94.2
  • cfed5b8 store tailscale.tgz and tailscaled.pid in XDG cache/runtime dirs instead of t...
  • 31d93e6 .github/workflows: run integration test if event is a push
  • 564fe38 chore: run make build to resolve linter error
  • 127dade fix: add missing parentheses to core.isDebug()
  • Additional commits viewable in compare view

Updates `Swatinem/rust-cache` from 2.8.2 to 2.9.1
Release notes

Sourced from Swatinem/rust-cache's releases.

v2.9.1

Fix regression in hash calculation

Full Changelog: https://github.com/Swatinem/rust-cache/compare/v2.9.0...v2.9.1

v2.9.0

What's Changed

New Contributors

Full Changelog: https://github.com/Swatinem/rust-cache/compare/v2.8.2...v2.9.0

Changelog

Sourced from Swatinem/rust-cache's changelog.

Changelog

2.9.1

  • Fix regression in hash calculation

2.9.0

  • Update to node24
  • Support running from within a nix shell
  • Consider all installed toolchains for cache key
  • Use case-insensitive comparison to determine exact cache hit

2.8.2

  • Don't overwrite env for cargo-metadata call

2.8.1

  • Set empty CARGO_ENCODED_RUSTFLAGS when retrieving metadata
  • Various dependency updates

2.8.0

  • Add support for warpbuild cache provider
  • Add new cache-workspace-crates feature

2.7.8

  • Include CPU arch in the cache key

2.7.7

  • Also cache cargo install metadata

2.7.6

  • Allow opting out of caching $CARGO_HOME/bin
  • Add runner OS in cache key
  • Adds an option to do lookup-only of the cache

2.7.5

  • Support Cargo.lock format cargo-lock v4
  • Only run macOsWorkaround() on macOS

2.7.3

  • Work around upstream problem that causes cache saving to hang for minutes.

... (truncated)

Commits

Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`. [//]: # (dependabot-automerge-start) [//]: # (dependabot-automerge-end) ---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot show ignore conditions` will show all of the ignore conditions of the specified dependency - `@dependabot ignore major version` will close this group update PR and stop Dependabot creating any more for the specific dependency's major version (unless you unignore this specific dependency's major version or upgrade to it yourself) - `@dependabot ignore minor version` will close this group update PR and stop Dependabot creating any more for the specific dependency's minor version (unless you unignore this specific dependency's minor version or upgrade to it yourself) - `@dependabot ignore ` will close this group update PR and stop Dependabot creating any more for the specific dependency (unless you unignore this specific dependency or upgrade to it yourself) - `@dependabot unignore ` will remove all of the ignore conditions of the specified dependency - `@dependabot unignore ` will remove the ignore condition of the specified dependency and ignore conditions
Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/docker.yml | 4 ++-- .github/workflows/fix_lint.yaml | 2 +- .github/workflows/latest_deps.yml | 6 +++--- .github/workflows/tests.yml | 24 ++++++++++++------------ .github/workflows/twisted_trunk.yml | 6 +++--- 5 files changed, 21 insertions(+), 21 deletions(-) diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml index da9cd02c78f..051c66ebba5 100644 --- a/.github/workflows/docker.yml +++ b/.github/workflows/docker.yml @@ -54,7 +54,7 @@ jobs: password: ${{ secrets.GITHUB_TOKEN }} - name: Tailscale - uses: tailscale/github-action@53acf823325fe9ca47f4cdaa951f90b4b0de5bb9 # v4.1.1 + uses: tailscale/github-action@306e68a486fd2350f2bfc3b19fcd143891a4a2d8 # v4.1.2 with: oauth-client-id: ${{ secrets.TS_OAUTH_CLIENT_ID }} audience: ${{ secrets.TS_AUDIENCE }} @@ -151,7 +151,7 @@ jobs: password: ${{ secrets.GITHUB_TOKEN }} - name: Tailscale - uses: tailscale/github-action@53acf823325fe9ca47f4cdaa951f90b4b0de5bb9 # v4.1.1 + uses: tailscale/github-action@306e68a486fd2350f2bfc3b19fcd143891a4a2d8 # v4.1.2 with: oauth-client-id: ${{ secrets.TS_OAUTH_CLIENT_ID }} audience: ${{ secrets.TS_AUDIENCE }} diff --git a/.github/workflows/fix_lint.yaml b/.github/workflows/fix_lint.yaml index 4752b6afebd..aea55fe0cea 100644 --- a/.github/workflows/fix_lint.yaml +++ b/.github/workflows/fix_lint.yaml @@ -25,7 +25,7 @@ jobs: with: toolchain: ${{ env.RUST_VERSION }} components: clippy, rustfmt - - uses: Swatinem/rust-cache@779680da715d629ac1d338a641029a2f4372abb5 # v2.8.2 + - uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2.9.1 - name: Setup Poetry uses: matrix-org/setup-python-poetry@5bbf6603c5c930615ec8a29f1b5d7d258d905aa4 # v2.0.0 diff --git a/.github/workflows/latest_deps.yml b/.github/workflows/latest_deps.yml index a85bad9d743..746223ef4af 100644 --- a/.github/workflows/latest_deps.yml +++ b/.github/workflows/latest_deps.yml @@ -47,7 +47,7 @@ jobs: uses: dtolnay/rust-toolchain@e97e2d8cc328f1b50210efc529dca0028893a2d9 # master with: toolchain: ${{ env.RUST_VERSION }} - - uses: Swatinem/rust-cache@779680da715d629ac1d338a641029a2f4372abb5 # v2.8.2 + - uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2.9.1 # The dev dependencies aren't exposed in the wheel metadata (at least with current # poetry-core versions), so we install with poetry. @@ -83,7 +83,7 @@ jobs: uses: dtolnay/rust-toolchain@e97e2d8cc328f1b50210efc529dca0028893a2d9 # master with: toolchain: ${{ env.RUST_VERSION }} - - uses: Swatinem/rust-cache@779680da715d629ac1d338a641029a2f4372abb5 # v2.8.2 + - uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2.9.1 - run: sudo apt-get -qq install xmlsec1 - name: Set up PostgreSQL ${{ matrix.postgres-version }} @@ -157,7 +157,7 @@ jobs: uses: dtolnay/rust-toolchain@e97e2d8cc328f1b50210efc529dca0028893a2d9 # master with: toolchain: ${{ env.RUST_VERSION }} - - uses: Swatinem/rust-cache@779680da715d629ac1d338a641029a2f4372abb5 # v2.8.2 + - uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2.9.1 - name: Ensure sytest runs `pip install` # Delete the lockfile so sytest will `pip install` rather than `poetry install` diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index c3dd8b14e56..4ec4df96e43 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -91,7 +91,7 @@ jobs: uses: dtolnay/rust-toolchain@e97e2d8cc328f1b50210efc529dca0028893a2d9 # master with: toolchain: ${{ env.RUST_VERSION }} - - uses: Swatinem/rust-cache@779680da715d629ac1d338a641029a2f4372abb5 # v2.8.2 + - uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2.9.1 - uses: matrix-org/setup-python-poetry@5bbf6603c5c930615ec8a29f1b5d7d258d905aa4 # v2.0.0 with: python-version: "3.x" @@ -157,7 +157,7 @@ jobs: uses: dtolnay/rust-toolchain@e97e2d8cc328f1b50210efc529dca0028893a2d9 # master with: toolchain: ${{ env.RUST_VERSION }} - - uses: Swatinem/rust-cache@779680da715d629ac1d338a641029a2f4372abb5 # v2.8.2 + - uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2.9.1 - name: Setup Poetry uses: matrix-org/setup-python-poetry@5bbf6603c5c930615ec8a29f1b5d7d258d905aa4 # v2.0.0 @@ -221,7 +221,7 @@ jobs: with: components: clippy toolchain: ${{ env.RUST_VERSION }} - - uses: Swatinem/rust-cache@779680da715d629ac1d338a641029a2f4372abb5 # v2.8.2 + - uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2.9.1 - run: cargo clippy -- -D warnings @@ -240,7 +240,7 @@ jobs: with: toolchain: nightly-2026-02-01 components: clippy - - uses: Swatinem/rust-cache@779680da715d629ac1d338a641029a2f4372abb5 # v2.8.2 + - uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2.9.1 - run: cargo clippy --all-features -- -D warnings @@ -257,7 +257,7 @@ jobs: uses: dtolnay/rust-toolchain@e97e2d8cc328f1b50210efc529dca0028893a2d9 # master with: toolchain: ${{ env.RUST_VERSION }} - - uses: Swatinem/rust-cache@779680da715d629ac1d338a641029a2f4372abb5 # v2.8.2 + - uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2.9.1 - name: Setup Poetry uses: matrix-org/setup-python-poetry@5bbf6603c5c930615ec8a29f1b5d7d258d905aa4 # v2.0.0 @@ -296,7 +296,7 @@ jobs: # `.rustfmt.toml`. toolchain: nightly-2025-04-23 components: rustfmt - - uses: Swatinem/rust-cache@779680da715d629ac1d338a641029a2f4372abb5 # v2.8.2 + - uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2.9.1 - run: cargo fmt --check @@ -393,7 +393,7 @@ jobs: uses: dtolnay/rust-toolchain@e97e2d8cc328f1b50210efc529dca0028893a2d9 # master with: toolchain: ${{ env.RUST_VERSION }} - - uses: Swatinem/rust-cache@779680da715d629ac1d338a641029a2f4372abb5 # v2.8.2 + - uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2.9.1 - uses: matrix-org/setup-python-poetry@5bbf6603c5c930615ec8a29f1b5d7d258d905aa4 # v2.0.0 with: @@ -437,7 +437,7 @@ jobs: uses: dtolnay/rust-toolchain@e97e2d8cc328f1b50210efc529dca0028893a2d9 # master with: toolchain: ${{ env.RUST_VERSION }} - - uses: Swatinem/rust-cache@779680da715d629ac1d338a641029a2f4372abb5 # v2.8.2 + - uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2.9.1 # There aren't wheels for some of the older deps, so we need to install # their build dependencies @@ -552,7 +552,7 @@ jobs: uses: dtolnay/rust-toolchain@e97e2d8cc328f1b50210efc529dca0028893a2d9 # master with: toolchain: ${{ env.RUST_VERSION }} - - uses: Swatinem/rust-cache@779680da715d629ac1d338a641029a2f4372abb5 # v2.8.2 + - uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2.9.1 - name: Run SyTest run: /bootstrap.sh synapse @@ -703,7 +703,7 @@ jobs: uses: dtolnay/rust-toolchain@e97e2d8cc328f1b50210efc529dca0028893a2d9 # master with: toolchain: ${{ env.RUST_VERSION }} - - uses: Swatinem/rust-cache@779680da715d629ac1d338a641029a2f4372abb5 # v2.8.2 + - uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2.9.1 # We use `poetry` in `complement.sh` - uses: matrix-org/setup-python-poetry@5bbf6603c5c930615ec8a29f1b5d7d258d905aa4 # v2.0.0 @@ -822,7 +822,7 @@ jobs: uses: dtolnay/rust-toolchain@e97e2d8cc328f1b50210efc529dca0028893a2d9 # master with: toolchain: ${{ env.RUST_VERSION }} - - uses: Swatinem/rust-cache@779680da715d629ac1d338a641029a2f4372abb5 # v2.8.2 + - uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2.9.1 - run: cargo test @@ -842,7 +842,7 @@ jobs: uses: dtolnay/rust-toolchain@e97e2d8cc328f1b50210efc529dca0028893a2d9 # master with: toolchain: nightly-2022-12-01 - - uses: Swatinem/rust-cache@779680da715d629ac1d338a641029a2f4372abb5 # v2.8.2 + - uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2.9.1 - run: cargo bench --no-run diff --git a/.github/workflows/twisted_trunk.yml b/.github/workflows/twisted_trunk.yml index e39d64cfb2f..be73e5283b0 100644 --- a/.github/workflows/twisted_trunk.yml +++ b/.github/workflows/twisted_trunk.yml @@ -48,7 +48,7 @@ jobs: uses: dtolnay/rust-toolchain@e97e2d8cc328f1b50210efc529dca0028893a2d9 # master with: toolchain: ${{ env.RUST_VERSION }} - - uses: Swatinem/rust-cache@779680da715d629ac1d338a641029a2f4372abb5 # v2.8.2 + - uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2.9.1 - uses: matrix-org/setup-python-poetry@5bbf6603c5c930615ec8a29f1b5d7d258d905aa4 # v2.0.0 with: @@ -76,7 +76,7 @@ jobs: uses: dtolnay/rust-toolchain@e97e2d8cc328f1b50210efc529dca0028893a2d9 # master with: toolchain: ${{ env.RUST_VERSION }} - - uses: Swatinem/rust-cache@779680da715d629ac1d338a641029a2f4372abb5 # v2.8.2 + - uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2.9.1 - uses: matrix-org/setup-python-poetry@5bbf6603c5c930615ec8a29f1b5d7d258d905aa4 # v2.0.0 with: @@ -121,7 +121,7 @@ jobs: uses: dtolnay/rust-toolchain@e97e2d8cc328f1b50210efc529dca0028893a2d9 # master with: toolchain: ${{ env.RUST_VERSION }} - - uses: Swatinem/rust-cache@779680da715d629ac1d338a641029a2f4372abb5 # v2.8.2 + - uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2.9.1 - name: Patch dependencies # Note: The poetry commands want to create a virtualenv in /src/.venv/, From ec4b050bfbec0da72badee4c54304b2cc9b0408a Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 31 Mar 2026 10:09:03 +0000 Subject: [PATCH 110/121] Bump requests from 2.32.5 to 2.33.0 (#19610) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps [requests](https://github.com/psf/requests) from 2.32.5 to 2.33.0.
Release notes

Sourced from requests's releases.

v2.33.0

2.33.0 (2026-03-25)

Announcements

  • 📣 Requests is adding inline types. If you have a typed code base that uses Requests, please take a look at #7271. Give it a try, and report any gaps or feedback you may have in the issue. 📣

Security

  • CVE-2026-25645 requests.utils.extract_zipped_paths now extracts contents to a non-deterministic location to prevent malicious file replacement. This does not affect default usage of Requests, only applications calling the utility function directly.

Improvements

  • Migrated to a PEP 517 build system using setuptools. (#7012)

Bugfixes

  • Fixed an issue where an empty netrc entry could cause malformed authentication to be applied to Requests on Python 3.11+. (#7205)

Deprecations

  • Dropped support for Python 3.9 following its end of support. (#7196)

Documentation

  • Various typo fixes and doc improvements.

New Contributors

Full Changelog: https://github.com/psf/requests/blob/main/HISTORY.md#2330-2026-03-25

Changelog

Sourced from requests's changelog.

2.33.0 (2026-03-25)

Announcements

  • 📣 Requests is adding inline types. If you have a typed code base that uses Requests, please take a look at #7271. Give it a try, and report any gaps or feedback you may have in the issue. 📣

Security

  • CVE-2026-25645 requests.utils.extract_zipped_paths now extracts contents to a non-deterministic location to prevent malicious file replacement. This does not affect default usage of Requests, only applications calling the utility function directly.

Improvements

  • Migrated to a PEP 517 build system using setuptools. (#7012)

Bugfixes

  • Fixed an issue where an empty netrc entry could cause malformed authentication to be applied to Requests on Python 3.11+. (#7205)

Deprecations

  • Dropped support for Python 3.9 following its end of support. (#7196)

Documentation

  • Various typo fixes and doc improvements.
Commits
  • bc04dfd v2.33.0
  • 66d21cb Merge commit from fork
  • 8b9bc8f Move badges to top of README (#7293)
  • e331a28 Remove unused extraction call (#7292)
  • 753fd08 docs: fix FAQ grammar in httplib2 example
  • 774a0b8 docs(socks): same block as other sections
  • 9c72a41 Bump github/codeql-action from 4.33.0 to 4.34.1
  • ebf7190 Bump github/codeql-action from 4.32.0 to 4.33.0
  • 0e4ae38 docs: exclude Response.is_permanent_redirect from API docs (#7244)
  • d568f47 docs: clarify Quickstart POST example (#6960)
  • Additional commits viewable in compare view

[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=requests&package-manager=pip&previous-version=2.32.5&new-version=2.33.0)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores) Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`. [//]: # (dependabot-automerge-start) [//]: # (dependabot-automerge-end) ---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot show ignore conditions` will show all of the ignore conditions of the specified dependency - `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself) You can disable automated security fix PRs for this repo from the [Security Alerts page](https://github.com/element-hq/synapse/network/alerts).
Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- poetry.lock | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/poetry.lock b/poetry.lock index 36d3035277e..b6266bfb84a 100644 --- a/poetry.lock +++ b/poetry.lock @@ -2674,25 +2674,26 @@ typing-extensions = {version = ">=4.4.0", markers = "python_version < \"3.13\""} [[package]] name = "requests" -version = "2.32.5" +version = "2.33.0" description = "Python HTTP for Humans." optional = false -python-versions = ">=3.9" +python-versions = ">=3.10" groups = ["main", "dev"] files = [ - {file = "requests-2.32.5-py3-none-any.whl", hash = "sha256:2462f94637a34fd532264295e186976db0f5d453d1cdd31473c85a6a161affb6"}, - {file = "requests-2.32.5.tar.gz", hash = "sha256:dbba0bac56e100853db0ea71b82b4dfd5fe2bf6d3754a8893c3af500cec7d7cf"}, + {file = "requests-2.33.0-py3-none-any.whl", hash = "sha256:3324635456fa185245e24865e810cecec7b4caf933d7eb133dcde67d48cee69b"}, + {file = "requests-2.33.0.tar.gz", hash = "sha256:c7ebc5e8b0f21837386ad0e1c8fe8b829fa5f544d8df3b2253bff14ef29d7652"}, ] [package.dependencies] -certifi = ">=2017.4.17" +certifi = ">=2023.5.7" charset_normalizer = ">=2,<4" idna = ">=2.5,<4" -urllib3 = ">=1.21.1,<3" +urllib3 = ">=1.26,<3" [package.extras] socks = ["PySocks (>=1.5.6,!=1.5.7)"] -use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"] +test = ["PySocks (>=1.5.6,!=1.5.7)", "pytest (>=3)", "pytest-cov", "pytest-httpbin (==2.1.0)", "pytest-mock", "pytest-xdist"] +use-chardet-on-py3 = ["chardet (>=3.0.2,<8)"] [[package]] name = "requests-toolbelt" From 84b3e5aa24555fab34be8ec761cad04b21f8c05f Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 31 Mar 2026 10:20:36 +0000 Subject: [PATCH 111/121] Bump dorny/paths-filter from 3.0.2 to 4.0.1 (#19625) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps [dorny/paths-filter](https://github.com/dorny/paths-filter) from 3.0.2 to 4.0.1.
Release notes

Sourced from dorny/paths-filter's releases.

v4.0.1

What's Changed

New Contributors

Full Changelog: https://github.com/dorny/paths-filter/compare/v4.0.0...v4.0.1

v4.0.0

What's Changed

New Contributors

Full Changelog: https://github.com/dorny/paths-filter/compare/v3.0.3...v4.0.0

v3.0.3

What's Changed

New Contributors

Full Changelog: https://github.com/dorny/paths-filter/compare/v3...v3.0.3

Changelog

Sourced from dorny/paths-filter's changelog.

Changelog

v4.0.0

v3.0.3

v3.0.2

v3.0.1

v3.0.0

v2.11.1

v2.11.0

v2.10.2

v2.10.1

v2.10.0

v2.9.3

v2.9.2

v2.9.1

v2.9.0

... (truncated)

Commits
  • fbd0ab8 feat: add merge_group event support
  • efb1da7 feat: add dist/ freshness check to PR workflow
  • d8f7b06 Merge pull request #302 from dorny/issue-299
  • addbc14 Update README for v4
  • 9d7afb8 Update CHANGELOG for v4.0.0
  • 782470c Merge branch 'releases/v3'
  • d1c1ffe Update CHANGELOG for v3.0.3
  • ce10459 Merge pull request #294 from saschabratton/master
  • 5f40380 feat: update action runtime to node24
  • 668c092 Merge pull request #279 from wardpeet/patch-1
  • Additional commits viewable in compare view

[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=dorny/paths-filter&package-manager=github_actions&previous-version=3.0.2&new-version=4.0.1)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores) Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`. [//]: # (dependabot-automerge-start) [//]: # (dependabot-automerge-end) ---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot show ignore conditions` will show all of the ignore conditions of the specified dependency - `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself)
Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/tests.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 4ec4df96e43..488208291f6 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -26,7 +26,7 @@ jobs: linting: ${{ !startsWith(github.ref, 'refs/pull/') || steps.filter.outputs.linting }} linting_readme: ${{ !startsWith(github.ref, 'refs/pull/') || steps.filter.outputs.linting_readme }} steps: - - uses: dorny/paths-filter@de90cc6fb38fc0963ad72b210f1f284cd68cea36 # v3.0.2 + - uses: dorny/paths-filter@fbd0ab8f3e69293af611ebaee6363fc25e6d187d # v4.0.1 id: filter # We only check on PRs if: startsWith(github.ref, 'refs/pull/') From df1752ee7977eb52e8f6e69114250bfd80a71c1e Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 31 Mar 2026 10:21:37 +0000 Subject: [PATCH 112/121] Bump cryptography from 46.0.5 to 46.0.6 (#19622) Bumps [cryptography](https://github.com/pyca/cryptography) from 46.0.5 to 46.0.6.
Changelog

Sourced from cryptography's changelog.

46.0.6 - 2026-03-25


* **SECURITY ISSUE**: Fixed a bug where name constraints were not
applied
  to peer names during verification when the leaf certificate contains a
wildcard DNS SAN. Ordinary X.509 topologies are not affected by this
bug,
including those used by the Web PKI. Credit to **Oleh Konko (1seal)**
for
  reporting the issue. **CVE-2026-34073**

.. _v46-0-5:

Commits

[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=cryptography&package-manager=pip&previous-version=46.0.5&new-version=46.0.6)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores) Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`. [//]: # (dependabot-automerge-start) [//]: # (dependabot-automerge-end) ---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot show ignore conditions` will show all of the ignore conditions of the specified dependency - `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself) You can disable automated security fix PRs for this repo from the [Security Alerts page](https://github.com/element-hq/synapse/network/alerts).
Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- poetry.lock | 102 ++++++++++++++++++++++++++-------------------------- 1 file changed, 51 insertions(+), 51 deletions(-) diff --git a/poetry.lock b/poetry.lock index b6266bfb84a..89d470a1652 100644 --- a/poetry.lock +++ b/poetry.lock @@ -453,61 +453,61 @@ files = [ [[package]] name = "cryptography" -version = "46.0.5" +version = "46.0.6" description = "cryptography is a package which provides cryptographic recipes and primitives to Python developers." optional = false python-versions = "!=3.9.0,!=3.9.1,>=3.8" groups = ["main", "dev"] files = [ - {file = "cryptography-46.0.5-cp311-abi3-macosx_10_9_universal2.whl", hash = "sha256:351695ada9ea9618b3500b490ad54c739860883df6c1f555e088eaf25b1bbaad"}, - {file = "cryptography-46.0.5-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:c18ff11e86df2e28854939acde2d003f7984f721eba450b56a200ad90eeb0e6b"}, - {file = "cryptography-46.0.5-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:4d7e3d356b8cd4ea5aff04f129d5f66ebdc7b6f8eae802b93739ed520c47c79b"}, - {file = "cryptography-46.0.5-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:50bfb6925eff619c9c023b967d5b77a54e04256c4281b0e21336a130cd7fc263"}, - {file = "cryptography-46.0.5-cp311-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:803812e111e75d1aa73690d2facc295eaefd4439be1023fefc4995eaea2af90d"}, - {file = "cryptography-46.0.5-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:3ee190460e2fbe447175cda91b88b84ae8322a104fc27766ad09428754a618ed"}, - {file = "cryptography-46.0.5-cp311-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:f145bba11b878005c496e93e257c1e88f154d278d2638e6450d17e0f31e558d2"}, - {file = "cryptography-46.0.5-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:e9251e3be159d1020c4030bd2e5f84d6a43fe54b6c19c12f51cde9542a2817b2"}, - {file = "cryptography-46.0.5-cp311-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:47fb8a66058b80e509c47118ef8a75d14c455e81ac369050f20ba0d23e77fee0"}, - {file = "cryptography-46.0.5-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:4c3341037c136030cb46e4b1e17b7418ea4cbd9dd207e4a6f3b2b24e0d4ac731"}, - {file = "cryptography-46.0.5-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:890bcb4abd5a2d3f852196437129eb3667d62630333aacc13dfd470fad3aaa82"}, - {file = "cryptography-46.0.5-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:80a8d7bfdf38f87ca30a5391c0c9ce4ed2926918e017c29ddf643d0ed2778ea1"}, - {file = "cryptography-46.0.5-cp311-abi3-win32.whl", hash = "sha256:60ee7e19e95104d4c03871d7d7dfb3d22ef8a9b9c6778c94e1c8fcc8365afd48"}, - {file = "cryptography-46.0.5-cp311-abi3-win_amd64.whl", hash = "sha256:38946c54b16c885c72c4f59846be9743d699eee2b69b6988e0a00a01f46a61a4"}, - {file = "cryptography-46.0.5-cp314-cp314t-macosx_10_9_universal2.whl", hash = "sha256:94a76daa32eb78d61339aff7952ea819b1734b46f73646a07decb40e5b3448e2"}, - {file = "cryptography-46.0.5-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:5be7bf2fb40769e05739dd0046e7b26f9d4670badc7b032d6ce4db64dddc0678"}, - {file = "cryptography-46.0.5-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:fe346b143ff9685e40192a4960938545c699054ba11d4f9029f94751e3f71d87"}, - {file = "cryptography-46.0.5-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:c69fd885df7d089548a42d5ec05be26050ebcd2283d89b3d30676eb32ff87dee"}, - {file = "cryptography-46.0.5-cp314-cp314t-manylinux_2_28_ppc64le.whl", hash = "sha256:8293f3dea7fc929ef7240796ba231413afa7b68ce38fd21da2995549f5961981"}, - {file = "cryptography-46.0.5-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:1abfdb89b41c3be0365328a410baa9df3ff8a9110fb75e7b52e66803ddabc9a9"}, - {file = "cryptography-46.0.5-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:d66e421495fdb797610a08f43b05269e0a5ea7f5e652a89bfd5a7d3c1dee3648"}, - {file = "cryptography-46.0.5-cp314-cp314t-manylinux_2_34_aarch64.whl", hash = "sha256:4e817a8920bfbcff8940ecfd60f23d01836408242b30f1a708d93198393a80b4"}, - {file = "cryptography-46.0.5-cp314-cp314t-manylinux_2_34_ppc64le.whl", hash = "sha256:68f68d13f2e1cb95163fa3b4db4bf9a159a418f5f6e7242564fc75fcae667fd0"}, - {file = "cryptography-46.0.5-cp314-cp314t-manylinux_2_34_x86_64.whl", hash = "sha256:a3d1fae9863299076f05cb8a778c467578262fae09f9dc0ee9b12eb4268ce663"}, - {file = "cryptography-46.0.5-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:c4143987a42a2397f2fc3b4d7e3a7d313fbe684f67ff443999e803dd75a76826"}, - {file = "cryptography-46.0.5-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:7d731d4b107030987fd61a7f8ab512b25b53cef8f233a97379ede116f30eb67d"}, - {file = "cryptography-46.0.5-cp314-cp314t-win32.whl", hash = "sha256:c3bcce8521d785d510b2aad26ae2c966092b7daa8f45dd8f44734a104dc0bc1a"}, - {file = "cryptography-46.0.5-cp314-cp314t-win_amd64.whl", hash = "sha256:4d8ae8659ab18c65ced284993c2265910f6c9e650189d4e3f68445ef82a810e4"}, - {file = "cryptography-46.0.5-cp38-abi3-macosx_10_9_universal2.whl", hash = "sha256:4108d4c09fbbf2789d0c926eb4152ae1760d5a2d97612b92d508d96c861e4d31"}, - {file = "cryptography-46.0.5-cp38-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:7d1f30a86d2757199cb2d56e48cce14deddf1f9c95f1ef1b64ee91ea43fe2e18"}, - {file = "cryptography-46.0.5-cp38-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:039917b0dc418bb9f6edce8a906572d69e74bd330b0b3fea4f79dab7f8ddd235"}, - {file = "cryptography-46.0.5-cp38-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:ba2a27ff02f48193fc4daeadf8ad2590516fa3d0adeeb34336b96f7fa64c1e3a"}, - {file = "cryptography-46.0.5-cp38-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:61aa400dce22cb001a98014f647dc21cda08f7915ceb95df0c9eaf84b4b6af76"}, - {file = "cryptography-46.0.5-cp38-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:3ce58ba46e1bc2aac4f7d9290223cead56743fa6ab94a5d53292ffaac6a91614"}, - {file = "cryptography-46.0.5-cp38-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:420d0e909050490d04359e7fdb5ed7e667ca5c3c402b809ae2563d7e66a92229"}, - {file = "cryptography-46.0.5-cp38-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:582f5fcd2afa31622f317f80426a027f30dc792e9c80ffee87b993200ea115f1"}, - {file = "cryptography-46.0.5-cp38-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:bfd56bb4b37ed4f330b82402f6f435845a5f5648edf1ad497da51a8452d5d62d"}, - {file = "cryptography-46.0.5-cp38-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:a3d507bb6a513ca96ba84443226af944b0f7f47dcc9a399d110cd6146481d24c"}, - {file = "cryptography-46.0.5-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:9f16fbdf4da055efb21c22d81b89f155f02ba420558db21288b3d0035bafd5f4"}, - {file = "cryptography-46.0.5-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:ced80795227d70549a411a4ab66e8ce307899fad2220ce5ab2f296e687eacde9"}, - {file = "cryptography-46.0.5-cp38-abi3-win32.whl", hash = "sha256:02f547fce831f5096c9a567fd41bc12ca8f11df260959ecc7c3202555cc47a72"}, - {file = "cryptography-46.0.5-cp38-abi3-win_amd64.whl", hash = "sha256:556e106ee01aa13484ce9b0239bca667be5004efb0aabbed28d353df86445595"}, - {file = "cryptography-46.0.5-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:3b4995dc971c9fb83c25aa44cf45f02ba86f71ee600d81091c2f0cbae116b06c"}, - {file = "cryptography-46.0.5-pp311-pypy311_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:bc84e875994c3b445871ea7181d424588171efec3e185dced958dad9e001950a"}, - {file = "cryptography-46.0.5-pp311-pypy311_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:2ae6971afd6246710480e3f15824ed3029a60fc16991db250034efd0b9fb4356"}, - {file = "cryptography-46.0.5-pp311-pypy311_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:d861ee9e76ace6cf36a6a89b959ec08e7bc2493ee39d07ffe5acb23ef46d27da"}, - {file = "cryptography-46.0.5-pp311-pypy311_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:2b7a67c9cd56372f3249b39699f2ad479f6991e62ea15800973b956f4b73e257"}, - {file = "cryptography-46.0.5-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:8456928655f856c6e1533ff59d5be76578a7157224dbd9ce6872f25055ab9ab7"}, - {file = "cryptography-46.0.5.tar.gz", hash = "sha256:abace499247268e3757271b2f1e244b36b06f8515cf27c4d49468fc9eb16e93d"}, + {file = "cryptography-46.0.6-cp311-abi3-macosx_10_9_universal2.whl", hash = "sha256:64235194bad039a10bb6d2d930ab3323baaec67e2ce36215fd0952fad0930ca8"}, + {file = "cryptography-46.0.6-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:26031f1e5ca62fcb9d1fcb34b2b60b390d1aacaa15dc8b895a9ed00968b97b30"}, + {file = "cryptography-46.0.6-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:9a693028b9cbe51b5a1136232ee8f2bc242e4e19d456ded3fa7c86e43c713b4a"}, + {file = "cryptography-46.0.6-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:67177e8a9f421aa2d3a170c3e56eca4e0128883cf52a071a7cbf53297f18b175"}, + {file = "cryptography-46.0.6-cp311-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:d9528b535a6c4f8ff37847144b8986a9a143585f0540fbcb1a98115b543aa463"}, + {file = "cryptography-46.0.6-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:22259338084d6ae497a19bae5d4c66b7ca1387d3264d1c2c0e72d9e9b6a77b97"}, + {file = "cryptography-46.0.6-cp311-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:760997a4b950ff00d418398ad73fbc91aa2894b5c1db7ccb45b4f68b42a63b3c"}, + {file = "cryptography-46.0.6-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:3dfa6567f2e9e4c5dceb8ccb5a708158a2a871052fa75c8b78cb0977063f1507"}, + {file = "cryptography-46.0.6-cp311-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:cdcd3edcbc5d55757e5f5f3d330dd00007ae463a7e7aa5bf132d1f22a4b62b19"}, + {file = "cryptography-46.0.6-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:d4e4aadb7fc1f88687f47ca20bb7227981b03afaae69287029da08096853b738"}, + {file = "cryptography-46.0.6-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:2b417edbe8877cda9022dde3a008e2deb50be9c407eef034aeeb3a8b11d9db3c"}, + {file = "cryptography-46.0.6-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:380343e0653b1c9d7e1f55b52aaa2dbb2fdf2730088d48c43ca1c7c0abb7cc2f"}, + {file = "cryptography-46.0.6-cp311-abi3-win32.whl", hash = "sha256:bcb87663e1f7b075e48c3be3ecb5f0b46c8fc50b50a97cf264e7f60242dca3f2"}, + {file = "cryptography-46.0.6-cp311-abi3-win_amd64.whl", hash = "sha256:6739d56300662c468fddb0e5e291f9b4d084bead381667b9e654c7dd81705124"}, + {file = "cryptography-46.0.6-cp314-cp314t-macosx_10_9_universal2.whl", hash = "sha256:2ef9e69886cbb137c2aef9772c2e7138dc581fad4fcbcf13cc181eb5a3ab6275"}, + {file = "cryptography-46.0.6-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:7f417f034f91dcec1cb6c5c35b07cdbb2ef262557f701b4ecd803ee8cefed4f4"}, + {file = "cryptography-46.0.6-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d24c13369e856b94892a89ddf70b332e0b70ad4a5c43cf3e9cb71d6d7ffa1f7b"}, + {file = "cryptography-46.0.6-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:aad75154a7ac9039936d50cf431719a2f8d4ed3d3c277ac03f3339ded1a5e707"}, + {file = "cryptography-46.0.6-cp314-cp314t-manylinux_2_28_ppc64le.whl", hash = "sha256:3c21d92ed15e9cfc6eb64c1f5a0326db22ca9c2566ca46d845119b45b4400361"}, + {file = "cryptography-46.0.6-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:4668298aef7cddeaf5c6ecc244c2302a2b8e40f384255505c22875eebb47888b"}, + {file = "cryptography-46.0.6-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:8ce35b77aaf02f3b59c90b2c8a05c73bac12cea5b4e8f3fbece1f5fddea5f0ca"}, + {file = "cryptography-46.0.6-cp314-cp314t-manylinux_2_34_aarch64.whl", hash = "sha256:c89eb37fae9216985d8734c1afd172ba4927f5a05cfd9bf0e4863c6d5465b013"}, + {file = "cryptography-46.0.6-cp314-cp314t-manylinux_2_34_ppc64le.whl", hash = "sha256:ed418c37d095aeddf5336898a132fba01091f0ac5844e3e8018506f014b6d2c4"}, + {file = "cryptography-46.0.6-cp314-cp314t-manylinux_2_34_x86_64.whl", hash = "sha256:69cf0056d6947edc6e6760e5f17afe4bea06b56a9ac8a06de9d2bd6b532d4f3a"}, + {file = "cryptography-46.0.6-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:8e7304c4f4e9490e11efe56af6713983460ee0780f16c63f219984dab3af9d2d"}, + {file = "cryptography-46.0.6-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:b928a3ca837c77a10e81a814a693f2295200adb3352395fad024559b7be7a736"}, + {file = "cryptography-46.0.6-cp314-cp314t-win32.whl", hash = "sha256:97c8115b27e19e592a05c45d0dd89c57f81f841cc9880e353e0d3bf25b2139ed"}, + {file = "cryptography-46.0.6-cp314-cp314t-win_amd64.whl", hash = "sha256:c797e2517cb7880f8297e2c0f43bb910e91381339336f75d2c1c2cbf811b70b4"}, + {file = "cryptography-46.0.6-cp38-abi3-macosx_10_9_universal2.whl", hash = "sha256:12cae594e9473bca1a7aceb90536060643128bb274fcea0fc459ab90f7d1ae7a"}, + {file = "cryptography-46.0.6-cp38-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:639301950939d844a9e1c4464d7e07f902fe9a7f6b215bb0d4f28584729935d8"}, + {file = "cryptography-46.0.6-cp38-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:ed3775295fb91f70b4027aeba878d79b3e55c0b3e97eaa4de71f8f23a9f2eb77"}, + {file = "cryptography-46.0.6-cp38-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:8927ccfbe967c7df312ade694f987e7e9e22b2425976ddbf28271d7e58845290"}, + {file = "cryptography-46.0.6-cp38-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:b12c6b1e1651e42ab5de8b1e00dc3b6354fdfd778e7fa60541ddacc27cd21410"}, + {file = "cryptography-46.0.6-cp38-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:063b67749f338ca9c5a0b7fe438a52c25f9526b851e24e6c9310e7195aad3b4d"}, + {file = "cryptography-46.0.6-cp38-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:02fad249cb0e090b574e30b276a3da6a149e04ee2f049725b1f69e7b8351ec70"}, + {file = "cryptography-46.0.6-cp38-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:7e6142674f2a9291463e5e150090b95a8519b2fb6e6aaec8917dd8d094ce750d"}, + {file = "cryptography-46.0.6-cp38-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:456b3215172aeefb9284550b162801d62f5f264a081049a3e94307fe20792cfa"}, + {file = "cryptography-46.0.6-cp38-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:341359d6c9e68834e204ceaf25936dffeafea3829ab80e9503860dcc4f4dac58"}, + {file = "cryptography-46.0.6-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:9a9c42a2723999a710445bc0d974e345c32adfd8d2fac6d8a251fa829ad31cfb"}, + {file = "cryptography-46.0.6-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:6617f67b1606dfd9fe4dbfa354a9508d4a6d37afe30306fe6c101b7ce3274b72"}, + {file = "cryptography-46.0.6-cp38-abi3-win32.whl", hash = "sha256:7f6690b6c55e9c5332c0b59b9c8a3fb232ebf059094c17f9019a51e9827df91c"}, + {file = "cryptography-46.0.6-cp38-abi3-win_amd64.whl", hash = "sha256:79e865c642cfc5c0b3eb12af83c35c5aeff4fa5c672dc28c43721c2c9fdd2f0f"}, + {file = "cryptography-46.0.6-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:2ea0f37e9a9cf0df2952893ad145fd9627d326a59daec9b0802480fa3bcd2ead"}, + {file = "cryptography-46.0.6-pp311-pypy311_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:a3e84d5ec9ba01f8fd03802b2147ba77f0c8f2617b2aff254cedd551844209c8"}, + {file = "cryptography-46.0.6-pp311-pypy311_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:12f0fa16cc247b13c43d56d7b35287ff1569b5b1f4c5e87e92cc4fcc00cd10c0"}, + {file = "cryptography-46.0.6-pp311-pypy311_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:50575a76e2951fe7dbd1f56d181f8c5ceeeb075e9ff88e7ad997d2f42af06e7b"}, + {file = "cryptography-46.0.6-pp311-pypy311_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:90e5f0a7b3be5f40c3a0a0eafb32c681d8d2c181fc2a1bdabe9b3f611d9f6b1a"}, + {file = "cryptography-46.0.6-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:6728c49e3b2c180ef26f8e9f0a883a2c585638db64cf265b49c9ba10652d430e"}, + {file = "cryptography-46.0.6.tar.gz", hash = "sha256:27550628a518c5c6c903d84f637fbecf287f6cb9ced3804838a1295dc1fd0759"}, ] [package.dependencies] @@ -521,7 +521,7 @@ nox = ["nox[uv] (>=2024.4.15)"] pep8test = ["check-sdist", "click (>=8.0.1)", "mypy (>=1.14)", "ruff (>=0.11.11)"] sdist = ["build (>=1.0.0)"] ssh = ["bcrypt (>=3.1.5)"] -test = ["certifi (>=2024)", "cryptography-vectors (==46.0.5)", "pretend (>=0.7)", "pytest (>=7.4.0)", "pytest-benchmark (>=4.0)", "pytest-cov (>=2.10.1)", "pytest-xdist (>=3.5.0)"] +test = ["certifi (>=2024)", "cryptography-vectors (==46.0.6)", "pretend (>=0.7)", "pytest (>=7.4.0)", "pytest-benchmark (>=4.0)", "pytest-cov (>=2.10.1)", "pytest-xdist (>=3.5.0)"] test-randomorder = ["pytest-randomly"] [[package]] From 0ac72bc55ef0936bc08179e61a37bdaa19e521d8 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 31 Mar 2026 11:33:34 +0000 Subject: [PATCH 113/121] Bump phonenumbers from 9.0.25 to 9.0.26 in the minor-and-patches group (#19627) Bumps the minor-and-patches group with 1 update: [phonenumbers](https://github.com/daviddrysdale/python-phonenumbers). Updates `phonenumbers` from 9.0.25 to 9.0.26
Commits

[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=phonenumbers&package-manager=pip&previous-version=9.0.25&new-version=9.0.26)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores) Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`. [//]: # (dependabot-automerge-start) [//]: # (dependabot-automerge-end) ---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot show ignore conditions` will show all of the ignore conditions of the specified dependency - `@dependabot ignore major version` will close this group update PR and stop Dependabot creating any more for the specific dependency's major version (unless you unignore this specific dependency's major version or upgrade to it yourself) - `@dependabot ignore minor version` will close this group update PR and stop Dependabot creating any more for the specific dependency's minor version (unless you unignore this specific dependency's minor version or upgrade to it yourself) - `@dependabot ignore ` will close this group update PR and stop Dependabot creating any more for the specific dependency (unless you unignore this specific dependency or upgrade to it yourself) - `@dependabot unignore ` will remove all of the ignore conditions of the specified dependency - `@dependabot unignore ` will remove the ignore condition of the specified dependency and ignore conditions
Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- poetry.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/poetry.lock b/poetry.lock index 89d470a1652..06746a8a6fb 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1889,14 +1889,14 @@ tests = ["pytest (>=9)", "typing-extensions (>=4.15)"] [[package]] name = "phonenumbers" -version = "9.0.25" +version = "9.0.26" description = "Python version of Google's common library for parsing, formatting, storing and validating international phone numbers." optional = false python-versions = ">=2.5" groups = ["main"] files = [ - {file = "phonenumbers-9.0.25-py2.py3-none-any.whl", hash = "sha256:b1fd6c20d588f5bcd40af3899d727a9f536364211ec6eac554fcd75ca58992a3"}, - {file = "phonenumbers-9.0.25.tar.gz", hash = "sha256:a5f236fa384c6a77378d7836c8e486ade5f984ad2e8e6cc0dbe5124315cdc81b"}, + {file = "phonenumbers-9.0.26-py2.py3-none-any.whl", hash = "sha256:ff473da5712965b6c7f7a31cbff8255864df694eb48243771133ecb761e807c1"}, + {file = "phonenumbers-9.0.26.tar.gz", hash = "sha256:9e582c827f0f5503cddeebef80099475a52ffa761551d8384099c7ec71298cbf"}, ] [[package]] From 51aa319463210960310655cfdbca1ea5c409f73c Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 31 Mar 2026 11:45:52 +0000 Subject: [PATCH 114/121] Bump once_cell from 1.21.3 to 1.21.4 in the patches group (#19623) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps the patches group with 1 update: [once_cell](https://github.com/matklad/once_cell). Updates `once_cell` from 1.21.3 to 1.21.4
Changelog

Sourced from once_cell's changelog.

1.21.4

  • Fix unsoundness in OnceCell::wait under --features=parking_lot, #295.

    If thread A calls wait, while thread B calls get_or_try_init(f), and, furthermore, f panics, the wait incorrectly returns and thread A observes uninitialized memory.

    Kudos to @​meng-xu-cs for a nice find!

Commits
  • 80fe900 Merge pull request #296 from matklad/matklad/plfix
  • a5e09c9 release 1.21.4
  • 2426d45 Merge pull request #295 from matklad/matklad/plfix
  • a144121 Merge pull request #290 from alexanderkjall/run-tests-with-no-default-features
  • 232a1a4 Merge pull request #292 from phil-opp/doc-fix
  • d31767b fix OnceCell::wait with features=parking_lot
  • 28d29ae Fix: sync::OnceCell::try_insert docs should import sync variant
  • 0f78fb0 make the command 'cargo test --no-default-features' work
  • fda60a6 Merge pull request #287 from AbeZbm/add-tests
  • b5b98c0 Add tests for OnceRef
  • See full diff in compare view

[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=once_cell&package-manager=cargo&previous-version=1.21.3&new-version=1.21.4)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores) Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`. [//]: # (dependabot-automerge-start) [//]: # (dependabot-automerge-end) ---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot show ignore conditions` will show all of the ignore conditions of the specified dependency - `@dependabot ignore major version` will close this group update PR and stop Dependabot creating any more for the specific dependency's major version (unless you unignore this specific dependency's major version or upgrade to it yourself) - `@dependabot ignore minor version` will close this group update PR and stop Dependabot creating any more for the specific dependency's minor version (unless you unignore this specific dependency's minor version or upgrade to it yourself) - `@dependabot ignore ` will close this group update PR and stop Dependabot creating any more for the specific dependency (unless you unignore this specific dependency or upgrade to it yourself) - `@dependabot unignore ` will remove all of the ignore conditions of the specified dependency - `@dependabot unignore ` will remove the ignore condition of the specified dependency and ignore conditions
Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- Cargo.lock | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index a004747af5b..5cb2ab58af5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -748,9 +748,9 @@ dependencies = [ [[package]] name = "once_cell" -version = "1.21.3" +version = "1.21.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" [[package]] name = "openssl-probe" From b0fc0b7a612a42e6f15b87dee2a1db4c383645fb Mon Sep 17 00:00:00 2001 From: Eric Eastwood Date: Tue, 31 Mar 2026 07:14:27 -0500 Subject: [PATCH 115/121] Lower the database `statement_timeout` to 10m (#19604) Lower the database `statement_timeout` to 10m Part of https://github.com/element-hq/backend-internal/issues/223 The `statement_timeout` was first introduced in https://github.com/matrix-org/synapse/pull/15853 as a great sanity check starting point (any timeout is better than no timeout). This idea is spawning from [discussions on 2026-03-16](https://docs.google.com/document/d/12RZKPk3a4__JUSH9wYHODo9rRyKzsHg6BSCAcmqmbOU/edit?tab=t.0#bookmark=id.x9y4kx82lvaj) about `matrix.org` performance and specifically room event search which frequently causes long-running transactions that hold up vacuums and causes things to go wibbly. It was brought up again today in another `matrix.org` performance [discussion on 2026-03-25](https://docs.google.com/document/d/15h4901gAzGMAol2n1b71OAcesVMNhgQbPNb55xouluw/edit?tab=t.0#bookmark=id.b53a918ntk0j) and was surprised nothing came of this yet. ### Some things may break We expect that when/if things break, to add specific overrides where it matters. For reference, we already have a couple of overrides in the codebase: - When using [`create_index_in_background`](https://github.com/element-hq/synapse/blob/40d35a95e2ce56982f839f2d5f01bdad34e65453/synapse/storage/background_updates.py#L802-L804) - When [purging rooms](https://github.com/element-hq/synapse/blob/40d35a95e2ce56982f839f2d5f01bdad34e65453/synapse/storage/databases/main/purge_events.py#L166-L169) ### Going further Ideally, we could go even further. The `statement_timeout` for any database query coming from people's HTTP requests should be 60s or less (or lower, no one wants to wait that long anyway). For now we'll take this iterative step down to 10m. Another point of reference is that for example on `matrix.org`, we even time out the whole HTTP request after 180s. For reference, we also discussed propagating request cancellation to the database query (cancelling statements when the user goes away) which is a good additional thing we could do. @sandhose looked into this before and it is only a recent libpq / Postgres / psycopg3 feature, not available in psycopg2 (which we currently use). As an example of how weird cancellation can get, `psql` will spawn a new connection to `SELECT pg_cancel_backend();` when you `Ctrl+C`, see https://neon.com/blog/ctrl-c-in-psql-gives-me-the-heebie-jeebies ### Pull Request Checklist * [x] Pull request is based on the develop branch * [x] Pull request includes a [changelog file](https://element-hq.github.io/synapse/latest/development/contributing_guide.html#changelog). The entry should: - Be a short description of your change which makes sense to users. "Fixed a bug that prevented receiving messages from other servers." instead of "Moved X method from `EventStore` to `EventWorkerStore`.". - Use markdown where necessary, mostly for `code blocks`. - End with either a period (.) or an exclamation mark (!). - Start with a capital letter. - Feel free to credit yourself, by adding a sentence "Contributed by @github_username." or "Contributed by [Your Name]." to the end of the entry. * [x] [Code style](https://element-hq.github.io/synapse/latest/code_style.html) is correct (run the [linters](https://element-hq.github.io/synapse/latest/development/contributing_guide.html#run-the-linters)) --- changelog.d/19604.misc | 1 + synapse/storage/engines/postgres.py | 8 +++++--- 2 files changed, 6 insertions(+), 3 deletions(-) create mode 100644 changelog.d/19604.misc diff --git a/changelog.d/19604.misc b/changelog.d/19604.misc new file mode 100644 index 00000000000..65081117d61 --- /dev/null +++ b/changelog.d/19604.misc @@ -0,0 +1 @@ +Lower the Postgres database `statement_timeout` to 10m (previously 1h). diff --git a/synapse/storage/engines/postgres.py b/synapse/storage/engines/postgres.py index 03ecff27f07..7cd50fb8f17 100644 --- a/synapse/storage/engines/postgres.py +++ b/synapse/storage/engines/postgres.py @@ -31,6 +31,7 @@ IsolationLevel, ) from synapse.storage.types import Cursor +from synapse.util.duration import Duration if TYPE_CHECKING: from synapse.storage.database import LoggingDatabaseConnection @@ -54,14 +55,15 @@ def _disable_bytes_adapter(_: bytes) -> NoReturn: psycopg2.extensions.register_adapter(bytes, _disable_bytes_adapter) self.synchronous_commit: bool = database_config.get("synchronous_commit", True) - # Set the statement timeout to 1 hour by default. - # Any query taking more than 1 hour should probably be considered a bug; + # Set the statement timeout to 10 minutes by default. + # + # Any query taking more than 10 minutes should probably be considered a bug; # most of the time this is a sign that work needs to be split up or that # some degenerate query plan has been created and the client has probably # timed out/walked off anyway. # This is in milliseconds. self.statement_timeout: int | None = database_config.get( - "statement_timeout", 60 * 60 * 1000 + "statement_timeout", Duration(minutes=10).as_millis() ) self._version: int | None = None # unknown as yet From 72711a33290c0172d1f313348633f803ba5fb456 Mon Sep 17 00:00:00 2001 From: Quentin Gliech Date: Tue, 31 Mar 2026 14:24:05 +0200 Subject: [PATCH 116/121] 1.151.0rc1 --- CHANGES.md | 39 +++++++++++++++++++++++++++++++ changelog.d/19430.removal | 1 - changelog.d/19503.bugfix.1 | 1 - changelog.d/19503.bugfix.2 | 1 - changelog.d/19503.feature | 1 - changelog.d/19509.bugfix | 1 - changelog.d/19511.feature | 1 - changelog.d/19538.misc | 1 - changelog.d/19559.bugfix | 1 - changelog.d/19573.feature | 1 - changelog.d/19576.feature | 1 - changelog.d/19577.misc | 1 - changelog.d/19578.bugfix | 1 - changelog.d/19581.misc | 1 - changelog.d/19585.misc | 1 - changelog.d/19586.feature | 1 - changelog.d/19589.misc | 1 - changelog.d/19590.misc | 1 - changelog.d/19592.misc | 1 - changelog.d/19602.misc.1 | 1 - changelog.d/19602.misc.2 | 1 - changelog.d/19604.misc | 1 - changelog.d/19629.bugfix | 1 - debian/changelog | 6 +++++ pyproject.toml | 2 +- schema/synapse-config.schema.yaml | 2 +- 26 files changed, 47 insertions(+), 24 deletions(-) delete mode 100644 changelog.d/19430.removal delete mode 100644 changelog.d/19503.bugfix.1 delete mode 100644 changelog.d/19503.bugfix.2 delete mode 100644 changelog.d/19503.feature delete mode 100644 changelog.d/19509.bugfix delete mode 100644 changelog.d/19511.feature delete mode 100644 changelog.d/19538.misc delete mode 100644 changelog.d/19559.bugfix delete mode 100644 changelog.d/19573.feature delete mode 100644 changelog.d/19576.feature delete mode 100644 changelog.d/19577.misc delete mode 100644 changelog.d/19578.bugfix delete mode 100644 changelog.d/19581.misc delete mode 100644 changelog.d/19585.misc delete mode 100644 changelog.d/19586.feature delete mode 100644 changelog.d/19589.misc delete mode 100644 changelog.d/19590.misc delete mode 100644 changelog.d/19592.misc delete mode 100644 changelog.d/19602.misc.1 delete mode 100644 changelog.d/19602.misc.2 delete mode 100644 changelog.d/19604.misc delete mode 100644 changelog.d/19629.bugfix diff --git a/CHANGES.md b/CHANGES.md index 6dbee41e7cc..bcf66142f9d 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,3 +1,42 @@ +# Synapse 1.151.0rc1 (2026-03-31) + +## Features + +- Add stable support for [MSC4284](https://github.com/matrix-org/matrix-spec-proposals/pull/4284) Policy Servers. ([\#19503](https://github.com/element-hq/synapse/issues/19503)) +- Update and stabilize support for [MSC2666](https://github.com/matrix-org/matrix-spec-proposals/pull/2666): Get rooms in common with another user. Contributed by @tulir @ Beeper. ([\#19511](https://github.com/element-hq/synapse/issues/19511)) +- Updated experimental support for [MSC4388: Secure out-of-band channel for sign in with QR](https://github.com/matrix-org/matrix-spec-proposals/pull/4388). ([\#19573](https://github.com/element-hq/synapse/issues/19573)) +- Stabilize `room_version` and `encryption` fields in the space/room `/hierarchy` API (part of [MSC3266](https://github.com/matrix-org/matrix-spec-proposals/pull/3266)). ([\#19576](https://github.com/element-hq/synapse/issues/19576)) +- Introduce a [configuration option](https://element-hq.github.io/synapse/latest/usage/configuration/config_documentation.html#matrix_authentication_service) to allow using HTTP/2 over plaintext when Synapse connects to Matrix Authentication Service. ([\#19586](https://github.com/element-hq/synapse/issues/19586)) + +## Bugfixes + +- Fix [MSC4284](https://github.com/matrix-org/matrix-spec-proposals/pull/4284) Policy Servers implementation to skip signing `org.matrix.msc4284.policy` and `m.room.policy` state events. ([\#19503](https://github.com/element-hq/synapse/issues/19503)) +- Correctly apply [MSC4284](https://github.com/matrix-org/matrix-spec-proposals/pull/4284) Policy Server signatures to events when the sender and policy server have the same server name. ([\#19503](https://github.com/element-hq/synapse/issues/19503)) +- Allow Synapse to start up even when discovery fails for an OpenID Connect provider. ([\#19509](https://github.com/element-hq/synapse/issues/19509)) +- Fix quarantine media admin APIs sometimes returning inaccurate counts for remote media. ([\#19559](https://github.com/element-hq/synapse/issues/19559)) +- Fix `Build and push complement image` CI job not having `poetry` available for the Complement runner script. ([\#19578](https://github.com/element-hq/synapse/issues/19578)) +- Increase timeout for policy server requests to avoid repeated requests for checking media. ([\#19629](https://github.com/element-hq/synapse/issues/19629)) + +## Deprecations and Removals + +- 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. ([\#19430](https://github.com/element-hq/synapse/issues/19430)) + +## Internal Changes + +- Fix small comment typo in config output from the `demo/start.sh` script. ([\#19538](https://github.com/element-hq/synapse/issues/19538)) +- Add MSC3820 comment context to `RoomVersion` attributes. ([\#19577](https://github.com/element-hq/synapse/issues/19577)) +- Remove `redacted_because` from internal unsigned. ([\#19581](https://github.com/element-hq/synapse/issues/19581)) +- Prevent sending registration emails if registration is disabled. ([\#19585](https://github.com/element-hq/synapse/issues/19585)) +- Port `RoomVersion` to Rust. ([\#19589](https://github.com/element-hq/synapse/issues/19589)) +- Only show failing Complement tests in the formatted output in CI. ([\#19590](https://github.com/element-hq/synapse/issues/19590)) +- Ensure old Complement test files are removed when downloading a Complement checkout via `./scripts-dev/complement.sh`. ([\#19592](https://github.com/element-hq/synapse/issues/19592)) +- Update `HomeserverTestCase.pump()` docstring to demystify behavior (Twisted reactor/clock). ([\#19602](https://github.com/element-hq/synapse/issues/19602)) +- Deprecate `HomeserverTestCase.pump()` in favor of more direct `HomeserverTestCase.reactor.advance(...)` usage. ([\#19602](https://github.com/element-hq/synapse/issues/19602)) +- Lower the Postgres database `statement_timeout` to 10m (previously 1h). ([\#19604](https://github.com/element-hq/synapse/issues/19604)) + + + + # Synapse 1.150.0 (2026-03-24) No significant changes since 1.150.0rc1. diff --git a/changelog.d/19430.removal b/changelog.d/19430.removal deleted file mode 100644 index 8920bccc38a..00000000000 --- a/changelog.d/19430.removal +++ /dev/null @@ -1 +0,0 @@ -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/19503.bugfix.1 b/changelog.d/19503.bugfix.1 deleted file mode 100644 index 45e3ad603b2..00000000000 --- a/changelog.d/19503.bugfix.1 +++ /dev/null @@ -1 +0,0 @@ -Fix [MSC4284](https://github.com/matrix-org/matrix-spec-proposals/pull/4284) Policy Servers implementation to skip signing `org.matrix.msc4284.policy` and `m.room.policy` state events. \ No newline at end of file diff --git a/changelog.d/19503.bugfix.2 b/changelog.d/19503.bugfix.2 deleted file mode 100644 index 8a423ba785d..00000000000 --- a/changelog.d/19503.bugfix.2 +++ /dev/null @@ -1 +0,0 @@ -Correctly apply [MSC4284](https://github.com/matrix-org/matrix-spec-proposals/pull/4284) Policy Server signatures to events when the sender and policy server have the same server name. \ No newline at end of file diff --git a/changelog.d/19503.feature b/changelog.d/19503.feature deleted file mode 100644 index 20db8aeaae1..00000000000 --- a/changelog.d/19503.feature +++ /dev/null @@ -1 +0,0 @@ -Add stable support for [MSC4284](https://github.com/matrix-org/matrix-spec-proposals/pull/4284) Policy Servers. diff --git a/changelog.d/19509.bugfix b/changelog.d/19509.bugfix deleted file mode 100644 index 4c98f1f4ee9..00000000000 --- a/changelog.d/19509.bugfix +++ /dev/null @@ -1 +0,0 @@ -Allow Synapse to start up even when discovery fails for an OpenID Connect provider. \ No newline at end of file diff --git a/changelog.d/19511.feature b/changelog.d/19511.feature deleted file mode 100644 index bb9a38b9c13..00000000000 --- a/changelog.d/19511.feature +++ /dev/null @@ -1 +0,0 @@ -Update and stabilize support for [MSC2666](https://github.com/matrix-org/matrix-spec-proposals/pull/2666): Get rooms in common with another user. Contributed by @tulir @ Beeper. diff --git a/changelog.d/19538.misc b/changelog.d/19538.misc deleted file mode 100644 index 56d3d2744d0..00000000000 --- a/changelog.d/19538.misc +++ /dev/null @@ -1 +0,0 @@ -Fix small comment typo in config output from the `demo/start.sh` script. diff --git a/changelog.d/19559.bugfix b/changelog.d/19559.bugfix deleted file mode 100644 index f64a84e8e02..00000000000 --- a/changelog.d/19559.bugfix +++ /dev/null @@ -1 +0,0 @@ -Fix quarantine media admin APIs sometimes returning inaccurate counts for remote media. \ No newline at end of file diff --git a/changelog.d/19573.feature b/changelog.d/19573.feature deleted file mode 100644 index 69eca83681f..00000000000 --- a/changelog.d/19573.feature +++ /dev/null @@ -1 +0,0 @@ -Updated experimental support for [MSC4388: Secure out-of-band channel for sign in with QR](https://github.com/matrix-org/matrix-spec-proposals/pull/4388). diff --git a/changelog.d/19576.feature b/changelog.d/19576.feature deleted file mode 100644 index e346c717207..00000000000 --- a/changelog.d/19576.feature +++ /dev/null @@ -1 +0,0 @@ -Stabilize `room_version` and `encryption` fields in the space/room `/hierarchy` API (part of [MSC3266](https://github.com/matrix-org/matrix-spec-proposals/pull/3266)). \ No newline at end of file diff --git a/changelog.d/19577.misc b/changelog.d/19577.misc deleted file mode 100644 index d26790e490d..00000000000 --- a/changelog.d/19577.misc +++ /dev/null @@ -1 +0,0 @@ -Add MSC3820 comment context to `RoomVersion` attributes. diff --git a/changelog.d/19578.bugfix b/changelog.d/19578.bugfix deleted file mode 100644 index dbaf4be7e89..00000000000 --- a/changelog.d/19578.bugfix +++ /dev/null @@ -1 +0,0 @@ -Fix `Build and push complement image` CI job not having `poetry` available for the Complement runner script. diff --git a/changelog.d/19581.misc b/changelog.d/19581.misc deleted file mode 100644 index 02856f34978..00000000000 --- a/changelog.d/19581.misc +++ /dev/null @@ -1 +0,0 @@ -Remove `redacted_because` from internal unsigned. diff --git a/changelog.d/19585.misc b/changelog.d/19585.misc deleted file mode 100644 index 621cf782f14..00000000000 --- a/changelog.d/19585.misc +++ /dev/null @@ -1 +0,0 @@ -Prevent sending registration emails if registration is disabled. \ No newline at end of file diff --git a/changelog.d/19586.feature b/changelog.d/19586.feature deleted file mode 100644 index a10bd2d2ef1..00000000000 --- a/changelog.d/19586.feature +++ /dev/null @@ -1 +0,0 @@ -Introduce a [configuration option](https://element-hq.github.io/synapse/latest/usage/configuration/config_documentation.html#matrix_authentication_service) to allow using HTTP/2 over plaintext when Synapse connects to Matrix Authentication Service. diff --git a/changelog.d/19589.misc b/changelog.d/19589.misc deleted file mode 100644 index f3c15326c67..00000000000 --- a/changelog.d/19589.misc +++ /dev/null @@ -1 +0,0 @@ -Port `RoomVersion` to Rust. diff --git a/changelog.d/19590.misc b/changelog.d/19590.misc deleted file mode 100644 index b5357334ceb..00000000000 --- a/changelog.d/19590.misc +++ /dev/null @@ -1 +0,0 @@ -Only show failing Complement tests in the formatted output in CI. \ No newline at end of file diff --git a/changelog.d/19592.misc b/changelog.d/19592.misc deleted file mode 100644 index e3fa3a5416b..00000000000 --- a/changelog.d/19592.misc +++ /dev/null @@ -1 +0,0 @@ -Ensure old Complement test files are removed when downloading a Complement checkout via `./scripts-dev/complement.sh`. \ No newline at end of file diff --git a/changelog.d/19602.misc.1 b/changelog.d/19602.misc.1 deleted file mode 100644 index 222f26980db..00000000000 --- a/changelog.d/19602.misc.1 +++ /dev/null @@ -1 +0,0 @@ -Update `HomeserverTestCase.pump()` docstring to demystify behavior (Twisted reactor/clock). diff --git a/changelog.d/19602.misc.2 b/changelog.d/19602.misc.2 deleted file mode 100644 index 4cadad653cf..00000000000 --- a/changelog.d/19602.misc.2 +++ /dev/null @@ -1 +0,0 @@ -Deprecate `HomeserverTestCase.pump()` in favor of more direct `HomeserverTestCase.reactor.advance(...)` usage. diff --git a/changelog.d/19604.misc b/changelog.d/19604.misc deleted file mode 100644 index 65081117d61..00000000000 --- a/changelog.d/19604.misc +++ /dev/null @@ -1 +0,0 @@ -Lower the Postgres database `statement_timeout` to 10m (previously 1h). diff --git a/changelog.d/19629.bugfix b/changelog.d/19629.bugfix deleted file mode 100644 index 06a03d78199..00000000000 --- a/changelog.d/19629.bugfix +++ /dev/null @@ -1 +0,0 @@ -Increase timeout for policy server requests to avoid repeated requests for checking media. \ No newline at end of file diff --git a/debian/changelog b/debian/changelog index c8146764de1..36fa0e51402 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,3 +1,9 @@ +matrix-synapse-py3 (1.151.0~rc1) stable; urgency=medium + + * New synapse release 1.151.0rc1. + + -- Synapse Packaging team Tue, 31 Mar 2026 12:23:34 +0000 + matrix-synapse-py3 (1.150.0) stable; urgency=medium * New synapse release 1.150.0. diff --git a/pyproject.toml b/pyproject.toml index c2f01826876..27cdbcb671b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "matrix-synapse" -version = "1.150.0" +version = "1.151.0rc1" description = "Homeserver for the Matrix decentralised comms protocol" readme = "README.rst" authors = [ diff --git a/schema/synapse-config.schema.yaml b/schema/synapse-config.schema.yaml index aaa306cee05..6acc5c37625 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.150/synapse-config.schema.json +$id: https://element-hq.github.io/synapse/schema/synapse/v1.151/synapse-config.schema.json type: object properties: modules: From e1a429ae2ec72a3cdb074eec8988813937026e3d Mon Sep 17 00:00:00 2001 From: Eric Eastwood Date: Tue, 31 Mar 2026 11:01:12 -0500 Subject: [PATCH 117/121] Run lint script to format Complement tests introduced in #19509 (#19636) Run `poetry run ./scripts-dev/lint.sh` which exposed some formatted Complement tests that were introduced in https://github.com/element-hq/synapse/pull/19509 There is no CI for this so it's easy to miss. --- changelog.d/19636.misc | 1 + complement/tests/internal/dockerutil/files.go | 20 ++++++++++++++++--- complement/tests/oidc_test.go | 5 ++++- 3 files changed, 22 insertions(+), 4 deletions(-) create mode 100644 changelog.d/19636.misc diff --git a/changelog.d/19636.misc b/changelog.d/19636.misc new file mode 100644 index 00000000000..9ea0f046038 --- /dev/null +++ b/changelog.d/19636.misc @@ -0,0 +1 @@ +Run lint script to format Complement tests introduced in [#19509](https://github.com/element-hq/synapse/pull/19509). diff --git a/complement/tests/internal/dockerutil/files.go b/complement/tests/internal/dockerutil/files.go index 62d2c557afb..e19f684368c 100644 --- a/complement/tests/internal/dockerutil/files.go +++ b/complement/tests/internal/dockerutil/files.go @@ -14,7 +14,13 @@ import ( // // Internally, produces an uncompressed single-file tape archive (tar) that is sent to the docker // daemon to be unpacked into the container filesystem. -func WriteFileIntoContainer(t *testing.T, docker *client.Client, containerID string, path string, data []byte) error { +func WriteFileIntoContainer( + t *testing.T, + docker *client.Client, + containerID string, + path string, + data []byte, +) error { // Create a fake/virtual tar file in memory that we can copy to the container // via https://stackoverflow.com/a/52131297/796832 var buf bytes.Buffer @@ -25,7 +31,11 @@ func WriteFileIntoContainer(t *testing.T, docker *client.Client, containerID str Size: int64(len(data)), }) if err != nil { - return fmt.Errorf("WriteIntoContainer: failed to write tarball header for %s: %v", path, err) + return fmt.Errorf( + "WriteIntoContainer: failed to write tarball header for %s: %v", + path, + err, + ) } _, err = tw.Write([]byte(data)) if err != nil { @@ -34,7 +44,11 @@ func WriteFileIntoContainer(t *testing.T, docker *client.Client, containerID str err = tw.Close() if err != nil { - return fmt.Errorf("WriteIntoContainer: failed to close tarball writer for %s: %w", path, err) + return fmt.Errorf( + "WriteIntoContainer: failed to close tarball writer for %s: %w", + path, + err, + ) } // Put our new fake file in the container volume diff --git a/complement/tests/oidc_test.go b/complement/tests/oidc_test.go index bddb382058b..deabf499506 100644 --- a/complement/tests/oidc_test.go +++ b/complement/tests/oidc_test.go @@ -93,7 +93,10 @@ func TestOIDCProviderUnavailable(t *testing.T) { // by the user's browser in order to start the login flow. queryParams := url.Values{} queryParams.Add("redirectUrl", "http://redirect.invalid/redirect") - res := unauthedClient.Do(t, "GET", []string{"_matrix", "client", "v3", "login", "sso", "redirect", "oidc-test_provider"}, + res := unauthedClient.Do( + t, + "GET", + []string{"_matrix", "client", "v3", "login", "sso", "redirect", "oidc-test_provider"}, client.WithQueries(queryParams), ) From 2e9b8202f0a1a8ceba9f02bb5ec227498d51dcbd Mon Sep 17 00:00:00 2001 From: Eric Eastwood Date: Tue, 31 Mar 2026 14:10:36 -0500 Subject: [PATCH 118/121] Document context for why increase timeout for policy server requests (#19633) See https://github.com/element-hq/synapse/pull/19629#discussion_r3011377886 --- changelog.d/19633.misc | 1 + synapse/handlers/room_policy.py | 7 ++++++- 2 files changed, 7 insertions(+), 1 deletion(-) create mode 100644 changelog.d/19633.misc diff --git a/changelog.d/19633.misc b/changelog.d/19633.misc new file mode 100644 index 00000000000..37efd5309fb --- /dev/null +++ b/changelog.d/19633.misc @@ -0,0 +1 @@ +Document context for why increase timeout for policy server requests. diff --git a/synapse/handlers/room_policy.py b/synapse/handlers/room_policy.py index fee3c6cfafb..01943e19915 100644 --- a/synapse/handlers/room_policy.py +++ b/synapse/handlers/room_policy.py @@ -223,11 +223,16 @@ async def ask_policy_server_to_sign_event( return # Ask the policy server to sign this event. - # We set a smallish timeout here as we don't want to block event sending too long. try: signature = await self._federation_client.ask_policy_server_to_sign_event( policy_server.server_name, event, + # We set a smallish timeout here as we don't want to block event sending + # too long. + # + # We were previously seeing regular timeouts with media + # scanning/checking when the timeout was set to 3s. 30s was chosen based + # on vibes and light real world testing. timeout=30000, ) # TODO: We can *probably* remove this when we remove unstable MSC4284 support. From cb6989514eccef02facce7568a8ae2c016c34fd5 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 2 Apr 2026 11:47:54 +0000 Subject: [PATCH 119/121] Bump pygments from 2.19.2 to 2.20.0 (#19632) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- poetry.lock | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/poetry.lock b/poetry.lock index 06746a8a6fb..d9a223dab24 100644 --- a/poetry.lock +++ b/poetry.lock @@ -2291,14 +2291,14 @@ urllib3 = ">=1.26.0" [[package]] name = "pygments" -version = "2.19.2" +version = "2.20.0" description = "Pygments is a syntax highlighting package written in Python." optional = false -python-versions = ">=3.8" +python-versions = ">=3.9" groups = ["dev"] files = [ - {file = "pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b"}, - {file = "pygments-2.19.2.tar.gz", hash = "sha256:636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887"}, + {file = "pygments-2.20.0-py3-none-any.whl", hash = "sha256:81a9e26dd42fd28a23a2d169d86d7ac03b46e2f8b59ed4698fb4785f946d0176"}, + {file = "pygments-2.20.0.tar.gz", hash = "sha256:6757cd03768053ff99f3039c1a36d6c0aa0b263438fcab17520b30a303a82b5f"}, ] [package.extras] From 62f23fed27808d11fdd3e3089838bec7f188d787 Mon Sep 17 00:00:00 2001 From: Neil Johnson Date: Thu, 2 Apr 2026 14:29:18 +0100 Subject: [PATCH 120/121] include workaround for macos (#19615) Co-authored-by: Andrew Morgan <1342360+anoadragon453@users.noreply.github.com> --- changelog.d/19615.doc | 1 + docs/development/contributing_guide.md | 15 +++++++++++++++ 2 files changed, 16 insertions(+) create mode 100644 changelog.d/19615.doc diff --git a/changelog.d/19615.doc b/changelog.d/19615.doc new file mode 100644 index 00000000000..9e28ac83127 --- /dev/null +++ b/changelog.d/19615.doc @@ -0,0 +1 @@ +Include a workaround for running the unit tests with SQLite under recent versions of MacOS. diff --git a/docs/development/contributing_guide.md b/docs/development/contributing_guide.md index ab506b5f05a..7e0dd4059d6 100644 --- a/docs/development/contributing_guide.md +++ b/docs/development/contributing_guide.md @@ -309,6 +309,21 @@ works without further arguments). Your Postgres account needs to be able to create databases; see the postgres docs for [`ALTER ROLE`](https://www.postgresql.org/docs/current/sql-alterrole.html). +### Running the tests on MacOS + +To run the unit tests with SQLite on MacOS you will need to swap out the default MacOS +python interpreter with the homebrew version. This is due to the default using a +locked down version of Python and sqlite3. After installing python from homebrew: + +``` + poetry env remove --all + poetry env use /opt/homebrew/bin/python3.13 + poetry install + poetry run trial tests +``` + +This example uses python 3.13, but choose whichever version you want. + ## Run the integration tests ([Sytest](https://github.com/matrix-org/sytest)). The integration tests are a more comprehensive suite of tests. They From 8291a493c735d56f2f81d69c544fffea0bb8088f Mon Sep 17 00:00:00 2001 From: Noah Markert Date: Fri, 3 Apr 2026 11:21:23 +0200 Subject: [PATCH 121/121] resolves #19403 Report the rust compiler version used in the prometheus metrics (#19643) # What is done? - resolves #19403 - Adds build-time Rust compiler detection and captures the rustc --version value during the build. - Exposes the captured compiler version from the Rust extension via a new Python-callable function. - Exports a new Prometheus metric for rustc version. # How to test? - compile `poetry install` - add `enable_metrics: true` and ```yaml resources: - compress: false names: - client - federation - metrics ``` to homeserver.yaml - start synapse - find the rustc version at `http://localhost:8008/_synapse/metrics` ### Pull Request Checklist * [x] Pull request is based on the develop branch * [x] Pull request includes a [changelog file](https://element-hq.github.io/synapse/latest/development/contributing_guide.html#changelog). The entry should: - Be a short description of your change which makes sense to users. "Fixed a bug that prevented receiving messages from other servers." instead of "Moved X method from `EventStore` to `EventWorkerStore`.". - Use markdown where necessary, mostly for `code blocks`. - End with either a period (.) or an exclamation mark (!). - Start with a capital letter. - Feel free to credit yourself, by adding a sentence "Contributed by @github_username." or "Contributed by [Your Name]." to the end of the entry. * [x] [Code style](https://element-hq.github.io/synapse/latest/code_style.html) is correct (run the [linters](https://element-hq.github.io/synapse/latest/development/contributing_guide.html#run-the-linters)) --------- Co-authored-by: Quentin Gliech --- Cargo.lock | 16 ++++++++++++++++ changelog.d/19643.feature | 1 + rust/Cargo.toml | 1 + rust/build.rs | 6 ++++++ rust/src/lib.rs | 9 +++++++++ synapse/metrics/__init__.py | 10 ++++++++-- synapse/synapse_rust/__init__.pyi | 1 + 7 files changed, 42 insertions(+), 2 deletions(-) create mode 100644 changelog.d/19643.feature diff --git a/Cargo.lock b/Cargo.lock index 5cb2ab58af5..e1747182e3b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1078,6 +1078,15 @@ version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d" +[[package]] +name = "rustc_version" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" +dependencies = [ + "semver", +] + [[package]] name = "rustls" version = "0.23.31" @@ -1169,6 +1178,12 @@ dependencies = [ "libc", ] +[[package]] +name = "semver" +version = "1.0.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d767eb0aabc880b29956c35734170f26ed551a859dbd361d140cdbeca61ab1e2" + [[package]] name = "serde" version = "1.0.228" @@ -1330,6 +1345,7 @@ dependencies = [ "pythonize", "regex", "reqwest", + "rustc_version", "serde", "serde_json", "sha2", diff --git a/changelog.d/19643.feature b/changelog.d/19643.feature new file mode 100644 index 00000000000..0851623fbfb --- /dev/null +++ b/changelog.d/19643.feature @@ -0,0 +1 @@ +Report the Rust compiler version used in the Prometheus metrics. Contributed by Noah Markert. \ No newline at end of file diff --git a/rust/Cargo.toml b/rust/Cargo.toml index 8199a4e02ba..bca2f6ed70f 100644 --- a/rust/Cargo.toml +++ b/rust/Cargo.toml @@ -64,3 +64,4 @@ default = ["extension-module"] [build-dependencies] blake2 = "0.10.4" hex = "0.4.3" +rustc_version = "0.4.1" diff --git a/rust/build.rs b/rust/build.rs index 8755f3bfa38..2a99a9b95d4 100644 --- a/rust/build.rs +++ b/rust/build.rs @@ -8,6 +8,7 @@ use std::path::PathBuf; use blake2::{Blake2b512, Digest}; +use rustc_version::version_meta; fn main() -> Result<(), std::io::Error> { let mut dirs = vec![PathBuf::from("src")]; @@ -48,6 +49,11 @@ fn main() -> Result<(), std::io::Error> { let hex_digest = hex::encode(hasher.finalize()); println!("cargo:rustc-env=SYNAPSE_RUST_DIGEST={hex_digest}"); + let rustc_version = version_meta() + .map(|v| v.short_version_string) + .unwrap_or_else(|_| "unknown".to_string()); + println!("cargo:rustc-env=SYNAPSE_RUSTC_VERSION={}", rustc_version,); + // The default rules don't pick up trivial changes to the workspace config // files, but we need to rebuild if those change to pick up the changed // hashes. diff --git a/rust/src/lib.rs b/rust/src/lib.rs index a6e01fce641..3b049a51b76 100644 --- a/rust/src/lib.rs +++ b/rust/src/lib.rs @@ -30,6 +30,14 @@ fn get_rust_file_digest() -> &'static str { env!("SYNAPSE_RUST_DIGEST") } +/// Returns the `rustc` version used when this native module was built. +/// +/// This value is embedded at build time, so it can be exported as a prometheus metrics. +#[pyfunction] +pub fn get_rustc_version() -> &'static str { + env!("SYNAPSE_RUSTC_VERSION") +} + /// Formats the sum of two numbers as string. #[pyfunction] #[pyo3(text_signature = "(a, b, /)")] @@ -50,6 +58,7 @@ fn reset_logging_config() { fn synapse_rust(py: Python<'_>, m: &Bound<'_, PyModule>) -> PyResult<()> { m.add_function(wrap_pyfunction!(sum_as_string, m)?)?; m.add_function(wrap_pyfunction!(get_rust_file_digest, m)?)?; + m.add_function(wrap_pyfunction!(get_rustc_version, m)?)?; m.add_function(wrap_pyfunction!(reset_logging_config, m)?)?; acl::register_module(py, m)?; diff --git a/synapse/metrics/__init__.py b/synapse/metrics/__init__.py index ba4334e3729..cf9aaa7f2d4 100644 --- a/synapse/metrics/__init__.py +++ b/synapse/metrics/__init__.py @@ -62,6 +62,7 @@ import synapse.metrics._reactor_metrics # noqa: F401 from synapse.metrics._gc import MIN_TIME_BETWEEN_GCS, install_gc_manager from synapse.metrics._types import Collector +from synapse.synapse_rust import get_rustc_version from synapse.types import StrSequence from synapse.util import SYNAPSE_VERSION @@ -69,6 +70,9 @@ METRICS_PREFIX = "/_synapse/metrics" +# Rust version used for compilation +RUSTC_VERSION = get_rustc_version() + HAVE_PROC_SELF_STAT = os.path.exists("/proc/self/stat") SERVER_NAME_LABEL = "server_name" @@ -672,15 +676,17 @@ def collect(self) -> Iterable[Metric]: # consider this process-level because all Synapse homeservers running in the process # will use the same Synapse version. build_info = Gauge( # type: ignore[missing-server-name-label] - "synapse_build_info", "Build information", ["pythonversion", "version", "osversion"] + "synapse_build_info", + "Build information", + ["pythonversion", "version", "osversion", "rustcversion"], ) build_info.labels( " ".join([platform.python_implementation(), platform.python_version()]), SYNAPSE_VERSION, " ".join([platform.system(), platform.release()]), + RUSTC_VERSION, ).set(1) - synapse_server_name_info = Gauge( "synapse_server_name_info", "Maps Synapse `server_name`s to the `instance`s they're hosted on", diff --git a/synapse/synapse_rust/__init__.pyi b/synapse/synapse_rust/__init__.pyi index d25c6091066..cb3eb7df07f 100644 --- a/synapse/synapse_rust/__init__.pyi +++ b/synapse/synapse_rust/__init__.pyi @@ -1,3 +1,4 @@ def sum_as_string(a: int, b: int) -> str: ... def get_rust_file_digest() -> str: ... def reset_logging_config() -> None: ... +def get_rustc_version() -> str: ...