Merge branch 'develop' into quenting/read-receipts/wrong-attribution

This commit is contained in:
Paul Chobert authored and GitHub committed 2026-10-02 11:14:34 +02:00
commit 9ca23cb10d
10 files changed
+680 -159

No files matched your search

+3 -3
View File
@@ -28,7 +28,7 @@ jobs:
steps:
- name: Set up Docker Buildx
id: buildx
uses: docker/setup-buildx-action@37fe631027851001ddb9b187196cc803df7f5f0e # v4.3.0
uses: docker/setup-buildx-action@f87e5991a6d7451dcb8d9637bfbc97413f497069 # v4.4.1
- name: Checkout repository
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
@@ -87,7 +87,7 @@ jobs:
- name: Build and push by digest
id: build
uses: docker/build-push-action@53b7df96c91f9c12dcc8a07bcb9ccacbed38856a # v7.3.0
uses: docker/build-push-action@c3c9e263c25d99ce0380d002d59b67737d91b0dc # v7.4.0
with:
push: true
labels: |
@@ -183,7 +183,7 @@ jobs:
password: ${{ steps.import-secrets.outputs.OCI_PASSWORD }}
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@37fe631027851001ddb9b187196cc803df7f5f0e # v4.3.0
uses: docker/setup-buildx-action@f87e5991a6d7451dcb8d9637bfbc97413f497069 # v4.4.1
- name: Install Cosign
uses: sigstore/cosign-installer@6f9f17788090df1f26f669e9d70d6ae9567deba6 # v4.1.2
+1 -1
View File
@@ -67,7 +67,7 @@ jobs:
- name: Set up Docker Buildx
id: buildx
uses: docker/setup-buildx-action@37fe631027851001ddb9b187196cc803df7f5f0e # v4.3.0
uses: docker/setup-buildx-action@f87e5991a6d7451dcb8d9637bfbc97413f497069 # v4.4.1
- name: Set up docker layer caching
uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
+1
View File
@@ -0,0 +1 @@
Bump `cryptography` from `46.0.7` to `50.0.1` to resolve `CVE-2026-69248`, `CVE-2026-69249` and `CVE-2026-69247`, none of which affected Synapse. Also bump `pyOpenSSL` from `26.0.0` to `26.4.0` for compatibility with newer `cryptography` versions.
+1
View File
@@ -0,0 +1 @@
Advertise support for Matrix v1.15.
+1
View File
@@ -0,0 +1 @@
Fix bug where membership events would not be returned in `/sync` when using lazy-loaded members and using an old token for `since`.
Generated
+99 -109
View File
@@ -1,4 +1,4 @@
# This file is automatically @generated by Poetry 2.4.1 and should not be changed by hand.
# This file is automatically @generated by Poetry 2.5.1 and should not be changed by hand.
[[package]]
name = "annotated-types"
@@ -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.12-py2.py3-none-any.whl", hash = "sha256:e9229ad7fde610b139dd12f5edbe97eab9ee78bfb85691247e767727850b99ab"},
{file = "authlib-1.6.12.tar.gz", hash = "sha256:0656d8482f28fc8221929d5f35b2bde5d13e10555ebc06b4561b0d622e83b1bd"},
@@ -62,7 +62,7 @@ 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\""
markers = "platform_machine != \"ppc64le\" and platform_machine != \"s390x\" and python_version < \"3.12\""
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"},
@@ -454,76 +454,66 @@ files = [
[[package]]
name = "cryptography"
version = "46.0.7"
version = "50.0.1"
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"
python-versions = "!=3.9.0,!=3.9.1,>=3.9"
groups = ["main", "dev"]
files = [
{file = "cryptography-46.0.7-cp311-abi3-macosx_10_9_universal2.whl", hash = "sha256:ea42cbe97209df307fdc3b155f1b6fa2577c0defa8f1f7d3be7d31d189108ad4"},
{file = "cryptography-46.0.7-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:b36a4695e29fe69215d75960b22577197aca3f7a25b9cf9d165dcfe9d80bc325"},
{file = "cryptography-46.0.7-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:5ad9ef796328c5e3c4ceed237a183f5d41d21150f972455a9d926593a1dcb308"},
{file = "cryptography-46.0.7-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:73510b83623e080a2c35c62c15298096e2a5dc8d51c3b4e1740211839d0dea77"},
{file = "cryptography-46.0.7-cp311-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:cbd5fb06b62bd0721e1170273d3f4d5a277044c47ca27ee257025146c34cbdd1"},
{file = "cryptography-46.0.7-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:420b1e4109cc95f0e5700eed79908cef9268265c773d3a66f7af1eef53d409ef"},
{file = "cryptography-46.0.7-cp311-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:24402210aa54baae71d99441d15bb5a1919c195398a87b563df84468160a65de"},
{file = "cryptography-46.0.7-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:8a469028a86f12eb7d2fe97162d0634026d92a21f3ae0ac87ed1c4a447886c83"},
{file = "cryptography-46.0.7-cp311-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:9694078c5d44c157ef3162e3bf3946510b857df5a3955458381d1c7cfc143ddb"},
{file = "cryptography-46.0.7-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:42a1e5f98abb6391717978baf9f90dc28a743b7d9be7f0751a6f56a75d14065b"},
{file = "cryptography-46.0.7-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:91bbcb08347344f810cbe49065914fe048949648f6bd5c2519f34619142bbe85"},
{file = "cryptography-46.0.7-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:5d1c02a14ceb9148cc7816249f64f623fbfee39e8c03b3650d842ad3f34d637e"},
{file = "cryptography-46.0.7-cp311-abi3-win32.whl", hash = "sha256:d23c8ca48e44ee015cd0a54aeccdf9f09004eba9fc96f38c911011d9ff1bd457"},
{file = "cryptography-46.0.7-cp311-abi3-win_amd64.whl", hash = "sha256:397655da831414d165029da9bc483bed2fe0e75dde6a1523ec2fe63f3c46046b"},
{file = "cryptography-46.0.7-cp314-cp314t-macosx_10_9_universal2.whl", hash = "sha256:d151173275e1728cf7839aaa80c34fe550c04ddb27b34f48c232193df8db5842"},
{file = "cryptography-46.0.7-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:db0f493b9181c7820c8134437eb8b0b4792085d37dbb24da050476ccb664e59c"},
{file = "cryptography-46.0.7-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:ebd6daf519b9f189f85c479427bbd6e9c9037862cf8fe89ee35503bd209ed902"},
{file = "cryptography-46.0.7-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:b7b412817be92117ec5ed95f880defe9cf18a832e8cafacf0a22337dc1981b4d"},
{file = "cryptography-46.0.7-cp314-cp314t-manylinux_2_28_ppc64le.whl", hash = "sha256:fbfd0e5f273877695cb93baf14b185f4878128b250cc9f8e617ea0c025dfb022"},
{file = "cryptography-46.0.7-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:ffca7aa1d00cf7d6469b988c581598f2259e46215e0140af408966a24cf086ce"},
{file = "cryptography-46.0.7-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:60627cf07e0d9274338521205899337c5d18249db56865f943cbe753aa96f40f"},
{file = "cryptography-46.0.7-cp314-cp314t-manylinux_2_34_aarch64.whl", hash = "sha256:80406c3065e2c55d7f49a9550fe0c49b3f12e5bfff5dedb727e319e1afb9bf99"},
{file = "cryptography-46.0.7-cp314-cp314t-manylinux_2_34_ppc64le.whl", hash = "sha256:c5b1ccd1239f48b7151a65bc6dd54bcfcc15e028c8ac126d3fada09db0e07ef1"},
{file = "cryptography-46.0.7-cp314-cp314t-manylinux_2_34_x86_64.whl", hash = "sha256:d5f7520159cd9c2154eb61eb67548ca05c5774d39e9c2c4339fd793fe7d097b2"},
{file = "cryptography-46.0.7-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:fcd8eac50d9138c1d7fc53a653ba60a2bee81a505f9f8850b6b2888555a45d0e"},
{file = "cryptography-46.0.7-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:65814c60f8cc400c63131584e3e1fad01235edba2614b61fbfbfa954082db0ee"},
{file = "cryptography-46.0.7-cp314-cp314t-win32.whl", hash = "sha256:fdd1736fed309b4300346f88f74cd120c27c56852c3838cab416e7a166f67298"},
{file = "cryptography-46.0.7-cp314-cp314t-win_amd64.whl", hash = "sha256:e06acf3c99be55aa3b516397fe42f5855597f430add9c17fa46bf2e0fb34c9bb"},
{file = "cryptography-46.0.7-cp38-abi3-macosx_10_9_universal2.whl", hash = "sha256:462ad5cb1c148a22b2e3bcc5ad52504dff325d17daf5df8d88c17dda1f75f2a4"},
{file = "cryptography-46.0.7-cp38-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:84d4cced91f0f159a7ddacad249cc077e63195c36aac40b4150e7a57e84fffe7"},
{file = "cryptography-46.0.7-cp38-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:128c5edfe5e5938b86b03941e94fac9ee793a94452ad1365c9fc3f4f62216832"},
{file = "cryptography-46.0.7-cp38-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:5e51be372b26ef4ba3de3c167cd3d1022934bc838ae9eaad7e644986d2a3d163"},
{file = "cryptography-46.0.7-cp38-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:cdf1a610ef82abb396451862739e3fc93b071c844399e15b90726ef7470eeaf2"},
{file = "cryptography-46.0.7-cp38-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:1d25aee46d0c6f1a501adcddb2d2fee4b979381346a78558ed13e50aa8a59067"},
{file = "cryptography-46.0.7-cp38-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:cdfbe22376065ffcf8be74dc9a909f032df19bc58a699456a21712d6e5eabfd0"},
{file = "cryptography-46.0.7-cp38-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:abad9dac36cbf55de6eb49badd4016806b3165d396f64925bf2999bcb67837ba"},
{file = "cryptography-46.0.7-cp38-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:935ce7e3cfdb53e3536119a542b839bb94ec1ad081013e9ab9b7cfd478b05006"},
{file = "cryptography-46.0.7-cp38-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:35719dc79d4730d30f1c2b6474bd6acda36ae2dfae1e3c16f2051f215df33ce0"},
{file = "cryptography-46.0.7-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:7bbc6ccf49d05ac8f7d7b5e2e2c33830d4fe2061def88210a126d130d7f71a85"},
{file = "cryptography-46.0.7-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:a1529d614f44b863a7b480c6d000fe93b59acee9c82ffa027cfadc77521a9f5e"},
{file = "cryptography-46.0.7-cp38-abi3-win32.whl", hash = "sha256:f247c8c1a1fb45e12586afbb436ef21ff1e80670b2861a90353d9b025583d246"},
{file = "cryptography-46.0.7-cp38-abi3-win_amd64.whl", hash = "sha256:506c4ff91eff4f82bdac7633318a526b1d1309fc07ca76a3ad182cb5b686d6d3"},
{file = "cryptography-46.0.7-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:fc9ab8856ae6cf7c9358430e49b368f3108f050031442eaeb6b9d87e4dcf4e4f"},
{file = "cryptography-46.0.7-pp311-pypy311_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:d3b99c535a9de0adced13d159c5a9cf65c325601aa30f4be08afd680643e9c15"},
{file = "cryptography-46.0.7-pp311-pypy311_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:d02c738dacda7dc2a74d1b2b3177042009d5cab7c7079db74afc19e56ca1b455"},
{file = "cryptography-46.0.7-pp311-pypy311_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:04959522f938493042d595a736e7dbdff6eb6cc2339c11465b3ff89343b65f65"},
{file = "cryptography-46.0.7-pp311-pypy311_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:3986ac1dee6def53797289999eabe84798ad7817f3e97779b5061a95b0ee4968"},
{file = "cryptography-46.0.7-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:258514877e15963bd43b558917bc9f54cf7cf866c38aa576ebf47a77ddbc43a4"},
{file = "cryptography-46.0.7.tar.gz", hash = "sha256:e4cfd68c5f3e0bfdad0d38e023239b96a2fe84146481852dffbcca442c245aa5"},
{file = "cryptography-50.0.1-cp311-abi3-macosx_11_0_arm64.whl", hash = "sha256:b8f852c65863251b9e3a1b8c150ce21e59b522dbb6a7d4bc80e680d38388e986"},
{file = "cryptography-50.0.1-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:53e279950892dc102c6b4e52af03ae5ea92fac572a1ddab78ca73a997f62b69f"},
{file = "cryptography-50.0.1-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:ff838d62ec1bfce4f9ba7fa16f4a7b554cd8d0c299e6be37502161a660c84eef"},
{file = "cryptography-50.0.1-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:e74591e283fe6eb956416c929eb58262a719fe0311fd9054c62c3350ed8760d8"},
{file = "cryptography-50.0.1-cp311-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:5fe002589592ed749ce77fe0695fcbd3500dd61d7d6db5858a7544c612fa8e45"},
{file = "cryptography-50.0.1-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:51593d180cf6d179bde5c5d065bed81386b1f381656ae7d042b7ffc87a9895ad"},
{file = "cryptography-50.0.1-cp311-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:359e62deae718bce96170e223fdcb6357e4fbd3bb7a3a75f4430763532560e49"},
{file = "cryptography-50.0.1-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:e2ca8fd1b6b4b82a1c4cb02841d0837e3c12336c2e24b520ab8ab3b969733d8f"},
{file = "cryptography-50.0.1-cp311-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:76de83fbd91ac49c0feaaa983d0748fd7a53176afac5fb3bf7478d244f0eb527"},
{file = "cryptography-50.0.1-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:51afcfceb15597cf2635068e4ac9a56b2abde622edde17f37d85fd7b5306497a"},
{file = "cryptography-50.0.1-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:be224a65493ec5b74a158ff22a5522ce4a5ca1e543c647a3a4730d4a09e5f959"},
{file = "cryptography-50.0.1-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:9ebcdd5519be9b652a46f507817a74591774fc3d6923ac364e4dfa64e36b291b"},
{file = "cryptography-50.0.1-cp311-abi3-win_amd64.whl", hash = "sha256:aed8db4f6d71c51efb89530e12d9464e7bf2923d46c3205dc794a2a93f8c0648"},
{file = "cryptography-50.0.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:30a125032e5642a21ff816e021152bd4e7e94f03eff3f4b7fca41cd22bc3110f"},
{file = "cryptography-50.0.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:a0b1a59e3a089064a0ec309e9428c8e3ae4e161419d20ac33600767e83fc658a"},
{file = "cryptography-50.0.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:8921d58f426793c5f1b47f0b59575780de9a095214958d0eb37d909593db8367"},
{file = "cryptography-50.0.1-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:a8f40ea47330e71b594a7e246898f93177c259490c63183dbaf9e571d71ed9a5"},
{file = "cryptography-50.0.1-cp314-cp314t-manylinux_2_28_ppc64le.whl", hash = "sha256:a255449073358275b64b67d3f595f268bbef70e72b6edb65e0c70c735bf739c9"},
{file = "cryptography-50.0.1-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:8df2de9102026855887e4587084f6eabd80ed0f345b8ad8a7ac27ab9bf4723e0"},
{file = "cryptography-50.0.1-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:ac02b07824d4d1001bd4367599f839c19cb171924c796e52c23508ac14c2c0cc"},
{file = "cryptography-50.0.1-cp314-cp314t-manylinux_2_34_aarch64.whl", hash = "sha256:cbf74a81765ee67413503ca6e26dcc4f6f5a519822436cc0a1b97aab6c1b8a17"},
{file = "cryptography-50.0.1-cp314-cp314t-manylinux_2_34_ppc64le.whl", hash = "sha256:16c5ecd954b3330ebfb6605eca4fd952da8bef376551d5cc264534e3770a9ee6"},
{file = "cryptography-50.0.1-cp314-cp314t-manylinux_2_34_x86_64.whl", hash = "sha256:79bf008d1f9af6071c797ad133e39915dfee7614f18f18f4db9072eb715064a3"},
{file = "cryptography-50.0.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:330fbb252391c596f1ae42c5754449dc924e6ad012dca8efe0d703f9f2d12ec6"},
{file = "cryptography-50.0.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:42be3bb70596b3abe4ac097b75be223e8b3ab614a0e5de068e3dcc54d71d6149"},
{file = "cryptography-50.0.1-cp314-cp314t-win_amd64.whl", hash = "sha256:f74455bb086a85d5e81246412602aaa97ed095e504cd40dd261ef50be42205bf"},
{file = "cryptography-50.0.1-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:ca83d00d9e69cd5eb63f2e69c3a5a59e0cecae5ae14c6ae0b35830fe3b37bad0"},
{file = "cryptography-50.0.1-cp39-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:05ba322c4da95b262a212c345af888ef2c37c88c0509756ea00a0e6d68850f23"},
{file = "cryptography-50.0.1-cp39-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:e22dfed744bd4002e909464cb23d2f0b05c6f3113a79ef2e9864a53db737c733"},
{file = "cryptography-50.0.1-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:4c4188f7c0cf655be5c06342b817ed0f9595b69ffa2b12026e5353eed29dea88"},
{file = "cryptography-50.0.1-cp39-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:2ebbfb0f1fed745e91796e3e1080a1440423fdae8ece1b995a1d80883a409054"},
{file = "cryptography-50.0.1-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:407fe2b6db00939c05c0e945e9914238f2f0a430974839429dafc82b1ee6bee5"},
{file = "cryptography-50.0.1-cp39-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:2b34d76a652ea2b6faf777c35df230c5637842cd904e04f16230c3f9f03e4361"},
{file = "cryptography-50.0.1-cp39-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:01f41478cf33fc605a6a089cd56d28b45c6c0b45a1928b61797f2621a04bac71"},
{file = "cryptography-50.0.1-cp39-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:fc3ed7ebd2a8c96f5b166de0ab9b624996bef3b07bbeb19364dfb78222c22c80"},
{file = "cryptography-50.0.1-cp39-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:9dde0a357190eb3b1da1bb9ab750e9c85cba82ca5977aa0836cbb94e92611239"},
{file = "cryptography-50.0.1-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:fd3718b960d0b5dd213cdf03f3bcb7000e69dda0de8b956061947ff6bcff5558"},
{file = "cryptography-50.0.1-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:2a93d05e34d5f67fba6f891fe85d929999baa7195e853923ea6d7576c9e68c5e"},
{file = "cryptography-50.0.1-cp39-abi3-win_amd64.whl", hash = "sha256:55d16b1ef3ee0958d893a977b19777887e546c9954ea81b200c3301a864013f2"},
{file = "cryptography-50.0.1-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:9cb3cb952cf5a8abd50c782a98a89d71699715e802fe349704b47f2425b42a94"},
{file = "cryptography-50.0.1-pp311-pypy311_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:5fe939deeb161024a6be98229c953b6591fef1f41214497a78fe793a244c017f"},
{file = "cryptography-50.0.1-pp311-pypy311_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:fb4b9672d389c738b175c4166e78310f8a70358886aacd9173ee03a85ffdc671"},
{file = "cryptography-50.0.1-pp311-pypy311_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:d63ae8f6481fec907ac0f588eee8a90aefde112c633131fe540e5711ddbb5a4e"},
{file = "cryptography-50.0.1-pp311-pypy311_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:804728ce710890870f3aaa344b2e161172d258d768ac139d02cfd9092d0d94e6"},
{file = "cryptography-50.0.1-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:693c99b49bd37d0d096e4334c10232c77248c415b98d35236094cdf96d57258b"},
{file = "cryptography-50.0.1.tar.gz", hash = "sha256:5dd9bda1c12b4162f6ff568eeb5e0ff956c28d14406e875cfe8a63a2d414ff20"},
]
[package.dependencies]
cffi = {version = ">=2.0.0", markers = "python_full_version >= \"3.9.0\" and platform_python_implementation != \"PyPy\""}
cffi = {version = ">=2.0.0", markers = "platform_python_implementation != \"PyPy\""}
typing-extensions = {version = ">=4.13.2", markers = "python_full_version < \"3.11.0\""}
[package.extras]
docs = ["sphinx (>=5.3.0)", "sphinx-inline-tabs", "sphinx-rtd-theme (>=3.0.0)"]
docstest = ["pyenchant (>=3)", "readme-renderer (>=30.0)", "sphinxcontrib-spelling (>=7.3.1)"]
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.7)", "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]]
name = "defusedxml"
@@ -532,7 +522,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"},
@@ -557,7 +547,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"},
@@ -607,7 +597,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.1-cp310-cp310-macosx_10_15_universal2.whl", hash = "sha256:f525734382a47f9828c9d6a1501522c78d5935466d8e2be1a41ba40ca5bb922b"},
{file = "hiredis-3.3.1-cp310-cp310-macosx_10_15_x86_64.whl", hash = "sha256:6e2e1024f0a021777740cb7c633a0efb2c4a4bc570f508223a8dcbcf79f99ef9"},
@@ -890,7 +880,7 @@ description = "Read metadata from Python packages"
optional = false
python-versions = ">=3.9"
groups = ["dev"]
markers = "python_version < \"3.12\" and platform_machine != \"ppc64le\" and platform_machine != \"s390x\""
markers = "platform_machine != \"ppc64le\" and platform_machine != \"s390x\" and python_version < \"3.12\""
files = [
{file = "importlib_metadata-8.7.1-py3-none-any.whl", hash = "sha256:5a1f80bf1daa489495071efbb095d75a634cf28a8bc299581244063b53176151"},
{file = "importlib_metadata-8.7.1.tar.gz", hash = "sha256:49fef1ae6440c182052f407c8d34a68f72efc36db9ca90dc0113398f2fdde8bb"},
@@ -931,7 +921,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"},
]
@@ -1123,7 +1113,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"},
@@ -1240,7 +1230,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.1.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:41dcc4c7b10484257cbd6c37b83ddb26df2b0e5aff5ac00d095689015af868ec"},
{file = "lxml-6.1.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:a31286dbb5e74c8e9a5344465b77ab4c5bd511a253b355b5ca2fae7e579fafec"},
@@ -1548,7 +1538,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"},
@@ -1833,7 +1823,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"},
]
@@ -2028,7 +2018,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"},
@@ -2046,7 +2036,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"},
]
@@ -2062,7 +2052,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"},
]
@@ -2302,14 +2292,14 @@ windows-terminal = ["colorama (>=0.4.6)"]
[[package]]
name = "pyjwt"
version = "2.13.0"
version = "2.15.1"
description = "JSON Web Token implementation in Python"
optional = false
python-versions = ">=3.9"
groups = ["dev"]
files = [
{file = "pyjwt-2.13.0-py3-none-any.whl", hash = "sha256:66adcc2aff09b3f1bbd95fc1e1577df8ac8723c978552fd43304c8a290ac5728"},
{file = "pyjwt-2.13.0.tar.gz", hash = "sha256:41571c89ca91598c79e8ef18a2d07367d4810fbbd6f637794879baf1b7703423"},
{file = "pyjwt-2.15.1-py3-none-any.whl", hash = "sha256:42d59d631f7768a1028a64c7ff581a9bf7519804daf91fc5b6c56e30eec5e193"},
{file = "pyjwt-2.15.1.tar.gz", hash = "sha256:4f259e80cdfb6b3fc18a7de51fd1ef9ec79652f25019bae68975ca2468a34df8"},
]
[package.dependencies]
@@ -2342,7 +2332,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"},
@@ -2392,19 +2382,19 @@ tests = ["hypothesis (>=3.27.0)", "pytest (>=7.4.0)", "pytest-cov (>=2.10.1)", "
[[package]]
name = "pyopenssl"
version = "26.0.0"
version = "26.4.0"
description = "Python wrapper module around the OpenSSL library"
optional = false
python-versions = ">=3.8"
python-versions = ">=3.9"
groups = ["main"]
files = [
{file = "pyopenssl-26.0.0-py3-none-any.whl", hash = "sha256:df94d28498848b98cc1c0ffb8ef1e71e40210d3b0a8064c9d29571ed2904bf81"},
{file = "pyopenssl-26.0.0.tar.gz", hash = "sha256:f293934e52936f2e3413b89c6ce36df66a0b34ae1ea3a053b8c5020ff2f513fc"},
{file = "pyopenssl-26.4.0-py3-none-any.whl", hash = "sha256:f0eb0cb2d581d3ad2b9c489468485e7f2ab6727d08401bcf9d824c3caddf3c1c"},
{file = "pyopenssl-26.4.0.tar.gz", hash = "sha256:28dfcce0162b9211413e26dfbfdf1d24317fbeba18fc93c12400a1856b2a0bc7"},
]
[package.dependencies]
cryptography = ">=46.0.0,<47"
typing-extensions = {version = ">=4.9", markers = "python_version < \"3.13\" and python_version >= \"3.8\""}
cryptography = ">=49.0.0,<51"
typing-extensions = {version = ">=4.9", markers = "python_version < \"3.13\""}
[package.extras]
docs = ["sphinx (!=5.2.0,!=5.2.0.post0,!=7.2.5)", "sphinx_rtd_theme"]
@@ -2474,7 +2464,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"},
@@ -2499,7 +2489,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"},
@@ -2527,7 +2517,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"},
@@ -2931,7 +2921,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.57.0-py2.py3-none-any.whl", hash = "sha256:812c8bf5ff3d2f0e89c82f5ce80ab3a6423e102729c4706af7413fd1eb480585"},
{file = "sentry_sdk-2.57.0.tar.gz", hash = "sha256:4be8d1e71c32fb27f79c577a337ac8912137bba4bcbc64a4ec1da4d6d8dc5199"},
@@ -3131,7 +3121,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"},
@@ -3147,7 +3137,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.24.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:efe85c4508adaf6c9e6f7fac2b1c3c9beb4b39b19c375519966335a70b28e64a"},
{file = "thrift-0.24.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a2baaec0c5cd7ba3eace54b26d81fdf0f5a85010468687cf4a131871d65abfed"},
@@ -3252,23 +3242,23 @@ markers = {main = "python_version < \"3.11\""}
[[package]]
name = "tornado"
version = "6.5.7"
version = "6.5.9"
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.7-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:148b2eb15c2c765a50796172c1e499649b35f30d2e3c3d3e15913cfa56bfb163"},
{file = "tornado-6.5.7-cp39-abi3-macosx_10_9_x86_64.whl", hash = "sha256:9da38de27f1da3b78a966f0dae12b5a1ea9afe72ca805d84ff06508272ddf100"},
{file = "tornado-6.5.7-cp39-abi3-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:8d759e71906ee783f8867b93bf26a265743da4c1e2f4a018464c1ba019862972"},
{file = "tornado-6.5.7-cp39-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8a46347a18f23fb92b396beebe0fb78f61dda0cc302445202c16203d8a18848b"},
{file = "tornado-6.5.7-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:7778b30bef919231265e91c69963ce0f49a1e9c07ac900bbe75b19ce2575ba92"},
{file = "tornado-6.5.7-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:e726f0c75da7726eec023aa62751ff8878bd2737e34fbdd33b1ae5897d2200f5"},
{file = "tornado-6.5.7-cp39-abi3-win32.whl", hash = "sha256:f8de3bf12d3efdd0cbe7c8887868198f8a91415e3f29fcf258d9b8eb7b1d9ae4"},
{file = "tornado-6.5.7-cp39-abi3-win_amd64.whl", hash = "sha256:de942f843533a039ef9fa3d9c88c7cd8a7c94553fb5ad0154270989b3d99a2c4"},
{file = "tornado-6.5.7-cp39-abi3-win_arm64.whl", hash = "sha256:ff934fce95643af5f11efdae618eaa73d469dc588641e5c8d19295a0c65c4796"},
{file = "tornado-6.5.7.tar.gz", hash = "sha256:66c513a76cda70d53907bc27cf1447557699c2e95aa48ba27a442ff61c3ddfc2"},
{file = "tornado-6.5.9-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:dea3bee433b73d6312d993ce89ad5f2b8d3ed9fde56e79ba5342b9edb78d342a"},
{file = "tornado-6.5.9-cp39-abi3-macosx_10_9_x86_64.whl", hash = "sha256:22bed49cdb55292d1d58f46008ba2cc3020e5b84995ef6ae749676c8b5309514"},
{file = "tornado-6.5.9-cp39-abi3-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:0cc0876a58eee9143476f24901b1b8eb4d52443283ade2b6bca650dd1da04f84"},
{file = "tornado-6.5.9-cp39-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:dc0d703cc7ec92b47ebfe236c97cd765c1f2e45744a2e156e6677e54c948274d"},
{file = "tornado-6.5.9-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:93f9ab0226ef625b94cacb7981a5af104f1735ea02f92282abe5c389109fb31d"},
{file = "tornado-6.5.9-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:5801bbf94dfde8d21087b6aa0b17f3a5dab6110dfe44e7e4492df5de393ac15b"},
{file = "tornado-6.5.9-cp39-abi3-win32.whl", hash = "sha256:dca9f377e80efe5dd4d52a2776b67492a81a39b2d931de169b6b36d0c1da444c"},
{file = "tornado-6.5.9-cp39-abi3-win_amd64.whl", hash = "sha256:f810730ac71eadf009e6e5cb6d534d096887310f5649cff19d7e0d19c0bf4ee7"},
{file = "tornado-6.5.9-cp39-abi3-win_arm64.whl", hash = "sha256:b5f4d92798337260c6d9f9f0e400c7539dd5e55cf93479a3f8508c9aa09c2b3b"},
{file = "tornado-6.5.9.tar.gz", hash = "sha256:4d868544ebdf2fc155239a74397f65ebfea9b4d3635296c96b5dfb0701c354f5"},
]
[[package]]
@@ -3334,7 +3324,7 @@ keyring = {version = ">=21.2.0", markers = "platform_machine != \"ppc64le\" and
packaging = ">=24.0"
readme-renderer = ">=35.0"
requests = ">=2.20"
requests-toolbelt = ">=0.8.0,<0.9.0 || >0.9.0"
requests-toolbelt = ">=0.8.0,!=0.9.0"
rfc3986 = ">=1.4.0"
rich = ">=12.0.0"
urllib3 = ">=1.26.0"
@@ -3389,7 +3379,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.12-py3-none-any.whl", hash = "sha256:c698fff24a0b82e8932ef18c5623496b0a8b5195e4e94bf8524800a00ca6b7c1"},
{file = "txredisapi-1.4.12.tar.gz", hash = "sha256:98e2440ff2e297c9048c5f9d516e7b1302d447cd9cd4568c986c35a8523de3c7"},
@@ -3615,14 +3605,14 @@ files = [
[[package]]
name = "urllib3"
version = "2.7.0"
version = "2.8.0"
description = "HTTP library with thread-safe connection pooling, file post, and more."
optional = false
python-versions = ">=3.10"
groups = ["main", "dev"]
files = [
{file = "urllib3-2.7.0-py3-none-any.whl", hash = "sha256:9fb4c81ebbb1ce9531cce37674bbc6f1360472bc18ca9a553ede278ef7276897"},
{file = "urllib3-2.7.0.tar.gz", hash = "sha256:231e0ec3b63ceb14667c67be60f2f2c40a518cb38b03af60abc813da26505f4c"},
{file = "urllib3-2.8.0-py3-none-any.whl", hash = "sha256:0cf3cae568d36aa9576b28dfb35f11328f1cb974ca7647d9475ebb86c75ac6e3"},
{file = "urllib3-2.8.0.tar.gz", hash = "sha256:63bf2ead4c879426ebf22ef2a781eeb4aa3b4ae798a0435506f8687fd5bb9b63"},
]
[package.extras]
@@ -3650,7 +3640,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"},
@@ -3671,7 +3661,7 @@ description = "Backport of pathlib-compatible object wrapper for zip files"
optional = false
python-versions = ">=3.9"
groups = ["dev"]
markers = "python_version < \"3.12\" and platform_machine != \"ppc64le\" and platform_machine != \"s390x\""
markers = "platform_machine != \"ppc64le\" and platform_machine != \"s390x\" and python_version < \"3.12\""
files = [
{file = "zipp-3.23.0-py3-none-any.whl", hash = "sha256:071652d6115ed432f5ce1d34c336c0adfd6a884660d1e9712a256d3d3bd4b14e"},
{file = "zipp-3.23.0.tar.gz", hash = "sha256:a07157588a12518c9d4034df3fbbee09c814741a33ff63c05fa29d26a2404166"},
+1
View File
@@ -158,6 +158,7 @@ async fn build_versions_response(
"v1.12".to_string(),
"v1.13".to_string(),
"v1.14".to_string(),
"v1.15".to_string(),
]),
unstable_features: unstable_feature_map,
})
+163 -43
View File
@@ -23,6 +23,7 @@ import itertools
import json
import logging
import os
from collections import OrderedDict
from typing import (
TYPE_CHECKING,
AbstractSet,
@@ -74,6 +75,7 @@ from synapse.types import (
MutableStateMap,
Requester,
RoomStreamToken,
StateKey,
StateMap,
StrCollection,
StreamKeyType,
@@ -130,6 +132,96 @@ LAZY_LOADED_PROFILE_FIELDS_CACHE_DIGEST_SIZE = 16
LAZY_LOADED_PROFILE_FIELDS_CACHE_DIGEST_KEY = os.urandom(32)
@attr.s(slots=True, auto_attribs=True)
class LazyLoadedMembersCache:
"""The membership events a lazy-loading client has been sent, so that later
syncs can leave them out. One per (user, device).
A member may only be left out if the client processed the response that
carried it. Each entry therefore records the `next_batch` position of the
response that carried it. A client that has processed a response syncs
from its `next_batch` next, so a request from `since` means the client
processed every response up to `since` and none after it. `start_request`
forgets the entries recorded after `since`. They were in responses the
client lost, by timing out a slow request and retrying it (#19978) or by
reloading from an older persisted token (#20278). Entries recorded at or
before `since` stay, so a client resuming from a token a few minutes old is
only sent again what it lost in those minutes. An initial sync forgets
everything.
Forgetting everything whenever `since` is before the newest entry would be
simpler, and `_newest` alone would suffice. But Element Web only persists
its sync token every few minutes and resumes from it on every reload, so
that would re-send every cached member on each reload.
The notifier may build several results for one long-poll and returns only
the first non-empty one. An empty result carries no members, so it records
nothing.
Leaving out a member the client never received loses it for good. A
`state_after` client does not apply state from the timeline, and a `state`
client only sees the membership changes that fall inside the timeline.
"""
# user_id -> (event_id of the membership we last sent, room position of the
# `next_batch` of the response that carried it). Least recently used first.
#
# Not an `LruCache`, as that cannot be iterated over and `start_request`
# needs to check every entry's position against `since`.
_sent: OrderedDict[str, tuple[str, RoomStreamToken]] = attr.Factory(OrderedDict)
# An upper bound on the positions of the entries, so that `start_request`
# need not look at them when nothing was recorded after `since`. That is the
# steady state, where `since` is the `next_batch` of the last response.
_newest: RoomStreamToken | None = None
def start_request(self, since_token: StreamToken | None) -> None:
"""Forget the members the client did not receive. Called at the start of
each sync request for this device."""
if since_token is None:
self._sent.clear()
self._newest = None
return
# Check for the common case where nothing has been recorded after
# `since`.
since = since_token.room_key
if self._newest is None or self._newest.is_before_or_eq(since):
return
# Remove all entries that were sent after `since`. This only has
# `LAZY_LOADED_MEMBERS_CACHE_MAX_SIZE` entries.
for user_id, (_, sent_at) in list(self._sent.items()):
if not sent_at.is_before_or_eq(since):
del self._sent[user_id]
# Everything left is at or before `since`.
self._newest = since
def was_sent(self, user_id: str, event_id: str) -> bool:
"""Whether `event_id` is the membership of `user_id` we last sent."""
entry = self._sent.get(user_id)
if entry is None or entry[0] != event_id:
return False
self._sent.move_to_end(user_id)
return True
def mark_sent(self, user_id: str, event_id: str, next_batch: StreamToken) -> None:
"""Record that `event_id` is being sent in the response ending at
`next_batch`."""
sent_at = next_batch.room_key
self._sent[user_id] = (event_id, sent_at)
self._sent.move_to_end(user_id)
while len(self._sent) > LAZY_LOADED_MEMBERS_CACHE_MAX_SIZE:
self._sent.popitem(last=False)
# Only move the bound forwards. Positions taken by this process only
# ever advance, but a response that started earlier may record its
# entries after a later one has.
if self._newest is None or self._newest.is_before_or_eq(sent_at):
self._newest = sent_at
SyncRequestKey = tuple[Any, ...]
@@ -345,9 +437,9 @@ class SyncHandler:
timeout=hs.config.caches.sync_response_cache_duration,
)
# ExpiringCache((User, Device)) -> LruCache(user_id => event_id)
# ExpiringCache((User, Device)) -> LazyLoadedMembersCache
self.lazy_loaded_members_cache: ExpiringCache[
tuple[str, str | None], LruCache[str, str]
tuple[str, str | None], LazyLoadedMembersCache
] = ExpiringCache(
cache_name="lazy_loaded_members_cache",
server_name=self.server_name,
@@ -1088,7 +1180,7 @@ class SyncHandler:
member_ids[hero_id]
for hero_id in summary["m.heroes"]
if (
cache.get(hero_id) != member_ids[hero_id]
not cache.was_sent(hero_id, member_ids[hero_id])
and hero_id not in existing_members
)
]
@@ -1096,27 +1188,21 @@ class SyncHandler:
missing_hero_state = await self.store.get_events(missing_hero_event_ids)
for s in missing_hero_state.values():
cache.set(s.state_key, s.event_id)
cache.mark_sent(s.state_key, s.event_id, now_token)
state[(EventTypes.Member, s.state_key)] = s
return summary
def get_lazy_loaded_members_cache(
self, cache_key: tuple[str, str | None]
) -> LruCache[str, str]:
# FIXME: This cache may be subject to losing members in the case that
# a sync is interrupted and retried, see https://github.com/element-hq/synapse/issues/19978
cache: LruCache[str, str] | None = self.lazy_loaded_members_cache.get(cache_key)
) -> LazyLoadedMembersCache:
cache = self.lazy_loaded_members_cache.get(cache_key)
if cache is None:
logger.debug("creating LruCache for %r", cache_key)
cache = LruCache(
max_size=LAZY_LOADED_MEMBERS_CACHE_MAX_SIZE,
clock=self.clock,
server_name=self.server_name,
)
logger.debug("creating LazyLoadedMembersCache for %r", cache_key)
cache = LazyLoadedMembersCache()
self.lazy_loaded_members_cache[cache_key] = cache
else:
logger.debug("found LruCache for %r", cache_key)
logger.debug("found LazyLoadedMembersCache for %r", cache_key)
return cache
def get_lazy_loaded_profile_fields_cache(
@@ -1155,6 +1241,7 @@ class SyncHandler:
sync_config: SyncConfig,
since_token: StreamToken | None,
end_token: StreamToken,
now_token: StreamToken,
full_state: bool,
joined: bool,
) -> MutableStateMap[EventBase]:
@@ -1169,6 +1256,7 @@ class SyncHandler:
end_token: Token of the end of the current batch. Normally this will be
the same as the global "now_token", but if the user has left the room,
the point just after their leave event.
now_token: The `next_batch` token of the response being built.
full_state: Whether to force returning the full state.
`lazy_load_members` still applies when `full_state` is `True`.
joined: whether the user is currently joined to the room
@@ -1254,6 +1342,11 @@ class SyncHandler:
# sync's timeline and the start of the current sync's timeline.
# See the docstring above for details.
state_ids: StateMap[str]
# The keys in `state_ids` whose state changed since `since_token`.
# These are sent whether or not the lazy-loaded members cache says
# the client has them. The rest are memberships fetched for
# timeline senders, which the client may already have.
changed_keys: set[StateKey] = set()
# We need to know whether the state we fetch may be partial, so check
# whether the room is partial stated *before* fetching it.
is_partial_state_room = await self.store.is_partial_state_room(room_id)
@@ -1273,7 +1366,10 @@ class SyncHandler:
# is indeed the case.
assert since_token is not None
state_ids = await self._compute_state_delta_for_incremental_sync(
(
state_ids,
changed_keys,
) = await self._compute_state_delta_for_incremental_sync(
room_id,
sync_config,
batch,
@@ -1309,30 +1405,28 @@ class SyncHandler:
cache_key = (sync_config.user.to_string(), sync_config.device_id)
cache = self.get_lazy_loaded_members_cache(cache_key)
# if it's a new sync sequence, then assume the client has had
# amnesia and doesn't want any recent lazy-loaded members
# de-duplicated.
if since_token is None:
logger.debug("clearing LruCache for %r", cache_key)
cache.clear()
else:
# only send members which aren't in our LruCache (either
# because they're new to this client or have been pushed out
# of the cache)
logger.debug("filtering state from %r...", state_ids)
state_ids = {
t: event_id
for t, event_id in state_ids.items()
if cache.get(t[1]) != event_id
}
logger.debug("...to %r", state_ids)
# Only send members which aren't in our cache (either because
# they're new to this client or have been pushed out of the
# cache). For an initial sync the cache has already been
# cleared.
#
# A state change since `since_token` is always sent, regardless
# of whether it was previously sent to the client (e.g due to a
# client retrying the request).
logger.debug("filtering state from %r...", state_ids)
state_ids = {
t: event_id
for t, event_id in state_ids.items()
if t in changed_keys or not cache.was_sent(t[1], event_id)
}
logger.debug("...to %r", state_ids)
# add any member IDs we are about to send into our LruCache
# add any member IDs we are about to send into our cache
for t, event_id in itertools.chain(
state_ids.items(), timeline_state.items()
):
if t[0] == EventTypes.Member:
cache.set(t[1], event_id)
cache.mark_sent(t[1], event_id, now_token)
state: dict[str, EventBase] = {}
if state_ids:
@@ -1505,14 +1599,16 @@ class SyncHandler:
members_to_fetch: set[str] | None,
timeline_state: StateMap[str],
joined: bool,
) -> StateMap[str]:
) -> tuple[StateMap[str], set[StateKey]]:
"""Calculate the state events to be included in an incremental sync response.
If lazy-loading of membership events is enabled (as indicated by
`members_to_fetch` being not-`None`), the result will include the membership
events for each member in `members_to_fetch`. The caller
(`compute_state_delta`) is responsible for keeping track of which membership
events we have already sent to the client, and hence ripping them out.
events we have already sent to the client, and hence ripping them out. It
may only rip out entries that are not state changes, so this also returns
the keys whose state changed since `since_token`.
Note that whether this returns the state at the start or the end of the
batch depends on `sync_config.use_state_after` (c.f. MSC4222).
@@ -1533,7 +1629,10 @@ class SyncHandler:
Returns:
A map from (type, state_key) to event_id, for each event that we believe
should be included in the `state` or `state_after` part of the sync response.
should be included in the `state` or `state_after` part of the sync
response, and the subset of its keys whose state changed since
`since_token`. The other keys are memberships fetched for timeline
senders, which the client may have been sent before.
"""
if members_to_fetch is not None:
# Lazy-loading is enabled. Only return the state that is needed.
@@ -1549,6 +1648,7 @@ class SyncHandler:
# timeline. If at the end we can just use the current state delta stream.
if sync_config.use_state_after:
delta_state_ids: MutableStateMap[str] = {}
changed_keys: set[StateKey] = set()
if members_to_fetch:
# We're lazy-loading, so the client might need some more member
@@ -1603,9 +1703,11 @@ class SyncHandler:
# Note that deltas are in stream ordering, so if there are
# multiple deltas for a given type/state_key we'll always pick
# the latest one.
delta_state_ids[(delta.event_type, delta.state_key)] = delta.event_id
key = (delta.event_type, delta.state_key)
delta_state_ids[key] = delta.event_id
changed_keys.add(key)
return delta_state_ids
return delta_state_ids, changed_keys
# For a non-gappy sync if the events in the timeline are simply a linear
# chain (i.e. no merging/branching of the graph), then we know the state
@@ -1653,7 +1755,9 @@ class SyncHandler:
await_full_state=False,
)
)
return state_ids
# A linear, non-gappy timeline means the only changes to the `state`
# are lazy-loaded members.
return state_ids, set()
if batch:
state_at_timeline_start = (
@@ -1712,7 +1816,16 @@ class SyncHandler:
lazy_load_members=lazy_load_members,
)
return state_ids
# Everything that differs from the state at the previous sync is a
# change. What is left is the memberships of timeline senders that
# `_calculate_state` adds back for lazy loading.
changed_keys = {
key
for key, event_id in state_ids.items()
if state_at_previous_sync.get(key) != event_id
}
return state_ids, changed_keys
async def _find_missing_partial_state_memberships(
self,
@@ -2650,7 +2763,13 @@ class SyncHandler:
"""
since_token = sync_result_builder.since_token
user_id = sync_result_builder.sync_config.user.to_string()
sync_config = sync_result_builder.sync_config
user_id = sync_config.user.to_string()
if sync_config.filter_collection.lazy_load_members():
self.get_lazy_loaded_members_cache(
(user_id, sync_config.device_id)
).start_request(since_token)
blocks_all_rooms = (
sync_result_builder.sync_config.filter_collection.blocks_all_rooms()
@@ -3318,6 +3437,7 @@ class SyncHandler:
sync_config,
since_token,
room_builder.end_token,
now_token,
full_state=full_state,
joined=room_builder.rtype == "joined",
)
+78 -2
View File
@@ -33,7 +33,9 @@ from synapse.api.room_versions import RoomVersion, RoomVersions
from synapse.events import EventBase
from synapse.events.snapshot import EventContext
from synapse.handlers.sync import (
LAZY_LOADED_MEMBERS_CACHE_MAX_SIZE,
LAZY_LOADED_PROFILE_FIELDS_CACHE_MAX_AGE,
LazyLoadedMembersCache,
SyncConfig,
SyncRequestKey,
SyncResult,
@@ -48,6 +50,7 @@ from synapse.types import (
MultiWriterStreamToken,
RoomStreamToken,
StreamKeyType,
StreamToken,
UserID,
create_requester,
)
@@ -2947,7 +2950,7 @@ class SyncStateAfterTestCase(tests.unittest.HomeserverTestCase):
# Calculating the incrementals state will return the second state, and not the
# first.
state = self.get_success(
state, _ = self.get_success(
self.sync_handler._compute_state_delta_for_incremental_sync(
room_id=joined_room,
sync_config=generate_sync_config(user, use_state_after=True),
@@ -2979,7 +2982,7 @@ class SyncStateAfterTestCase(tests.unittest.HomeserverTestCase):
since_token = self.hs.get_event_sources().get_current_token()
end_stream_token = self.hs.get_event_sources().get_current_token()
state = self.get_success(
state, _ = self.get_success(
self.sync_handler._compute_state_delta_for_incremental_sync(
room_id=joined_room,
sync_config=generate_sync_config(user, use_state_after=True),
@@ -2995,3 +2998,76 @@ class SyncStateAfterTestCase(tests.unittest.HomeserverTestCase):
)
self.assertEqual(state, {})
class LazyLoadedMembersCacheTestCase(tests.unittest.TestCase):
"""Direct tests of the per-device cache of lazy-loaded members sent."""
@staticmethod
def _token(stream: int) -> StreamToken:
"""A token whose room position is `stream`. Only the room position
matters to the cache."""
return StreamToken.START.copy_and_replace(
StreamKeyType.ROOM, RoomStreamToken(stream=stream)
)
def test_forgets_only_entries_after_since(self) -> None:
"""A request from `since` forgets the entries recorded after it and
keeps the rest."""
cache = LazyLoadedMembersCache()
cache.mark_sent("@a:test", "$a", self._token(5))
cache.mark_sent("@b:test", "$b", self._token(7))
# The client synced past both responses.
cache.start_request(self._token(7))
self.assertTrue(cache.was_sent("@a:test", "$a"))
self.assertTrue(cache.was_sent("@b:test", "$b"))
# The client lost the response that ended at 7.
cache.start_request(self._token(6))
self.assertTrue(cache.was_sent("@a:test", "$a"))
self.assertFalse(cache.was_sent("@b:test", "$b"))
# A later request from an even older token forgets the rest.
cache.start_request(self._token(4))
self.assertFalse(cache.was_sent("@a:test", "$a"))
def test_initial_sync_forgets_everything(self) -> None:
"""An initial sync has no `since`, and the client starts from nothing."""
cache = LazyLoadedMembersCache()
cache.mark_sent("@a:test", "$a", self._token(5))
cache.start_request(None)
self.assertFalse(cache.was_sent("@a:test", "$a"))
def test_different_event_is_not_sent(self) -> None:
"""An entry only covers the membership event it was recorded with, so
a changed membership counts as not sent."""
cache = LazyLoadedMembersCache()
cache.mark_sent("@a:test", "$a1", self._token(5))
self.assertFalse(cache.was_sent("@a:test", "$a2"))
def test_out_of_order_marks_are_still_forgotten(self) -> None:
"""A response that started earlier can record its entries after a
later one. Its entries must still be forgotten by a request from
before either response."""
cache = LazyLoadedMembersCache()
cache.mark_sent("@b:test", "$b", self._token(7))
cache.mark_sent("@a:test", "$a", self._token(5))
cache.start_request(self._token(6))
self.assertTrue(cache.was_sent("@a:test", "$a"))
self.assertFalse(cache.was_sent("@b:test", "$b"))
def test_evicts_least_recently_used(self) -> None:
"""The cache holds `LAZY_LOADED_MEMBERS_CACHE_MAX_SIZE` entries and
evicts the least recently used one. A hit counts as a use."""
cache = LazyLoadedMembersCache()
for i in range(LAZY_LOADED_MEMBERS_CACHE_MAX_SIZE):
cache.mark_sent(f"@u{i}:test", f"$u{i}", self._token(1))
# Touch the oldest entry so that it is no longer the eviction candidate.
self.assertTrue(cache.was_sent("@u0:test", "$u0"))
cache.mark_sent("@new:test", "$new", self._token(1))
self.assertTrue(cache.was_sent("@u0:test", "$u0"))
self.assertFalse(cache.was_sent("@u1:test", "$u1"))
self.assertTrue(cache.was_sent("@new:test", "$new"))
+332 -1
View File
@@ -45,7 +45,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
from tests.server import FakeChannel, TimedOutException
from tests.test_utils.event_injection import (
inject_event,
persist_message_and_state_event_in_one_batch,
@@ -1684,3 +1684,334 @@ class SyncStateAfterArchivedRoomTestCase(unittest.HomeserverTestCase):
],
[bob_member_event_id_at_leave],
)
class SyncLazyLoadedMembersCacheTestCase(unittest.HomeserverTestCase):
"""Tests for the per-device cache of lazy-loaded members already sent.
A membership left out of a response as already sent is lost for good if
the client never processed the response that carried it. The cache must
only trust a response once the client has synced from its `next_batch`.
"""
servlets = [
synapse.rest.admin.register_servlets,
room.register_servlets,
login.register_servlets,
sync.register_servlets,
]
def prepare(self, reactor: MemoryReactor, clock: Clock, hs: HomeServer) -> None:
self.alice = self.register_user("alice", "password")
self.alice_device = "alice_device"
self.alice_tok = self.login("alice", "password", device_id=self.alice_device)
self.bob = self.register_user("bob", "password")
self.bob_tok = self.login("bob", "password")
self.get_success(
hs.get_datastores().main.set_features_for_user(
self.alice, {ExperimentalFeature.MSC4222: True}
)
)
# Named room, so that hero calculation doesn't inject current
# membership state into the response and confuse the assertions.
self.room_id = self.helper.create_room_as(
self.alice, tok=self.alice_tok, extra_content={"name": "test room"}
)
self.helper.join(self.room_id, self.bob, tok=self.bob_tok)
# Push bob's join out of the initial sync's timeline, so that his
# membership has not been sent to the client when a test starts.
for i in range(10):
self.helper.send(self.room_id, body=f"pad {i}", tok=self.alice_tok)
def _sync_request(
self,
use_state_after: bool,
since: str | None = None,
*,
timeline_limit: int = 10,
timeline_not_types: list[str] | None = None,
timeout_ms: int = 0,
await_result: bool = True,
) -> FakeChannel:
"""Make a lazy-loading `/sync` request as alice."""
timeline_filter: JsonDict = {"limit": timeline_limit}
if timeline_not_types is not None:
timeline_filter["not_types"] = timeline_not_types
sync_filter: JsonDict = {
"room": {
"timeline": timeline_filter,
"state": {"lazy_load_members": True},
}
}
url = f"/sync?filter={json.dumps(sync_filter)}&timeout={timeout_ms}"
if use_state_after:
url += "&org.matrix.msc4222.use_state_after=true"
if since is not None:
url += f"&since={since}"
return self.make_request(
"GET", url, access_token=self.alice_tok, await_result=await_result
)
def _sync(
self,
use_state_after: bool,
since: str | None = None,
*,
timeline_limit: int = 10,
timeline_not_types: list[str] | None = None,
) -> JsonDict:
"""Sync as alice and return the response body."""
channel = self._sync_request(
use_state_after,
since,
timeline_limit=timeline_limit,
timeline_not_types=timeline_not_types,
)
self.assertEqual(channel.code, 200, channel.result)
return channel.json_body
def _room(self, response: JsonDict) -> JsonDict:
"""Extract the room dict for the test room from a sync response."""
rooms = response["rooms"].get("join", {})
self.assertIn(self.room_id, rooms, f"room missing from sync: {response}")
return rooms[self.room_id]
def _timeline_ids(self, response: JsonDict) -> list[str]:
return [e["event_id"] for e in self._room(response)["timeline"]["events"]]
def _state_events(
self, response: JsonDict, use_state_after: bool
) -> list[JsonDict]:
"""Extract the list of state events from the test room in a sync response."""
key = "org.matrix.msc4222.state_after" if use_state_after else "state"
return self._room(response)[key]["events"]
def _state_ids(self, response: JsonDict, use_state_after: bool) -> list[str]:
"""Extract the list of state event IDs from the test room in a sync
response."""
return [e["event_id"] for e in self._state_events(response, use_state_after)]
def _members_in_state(self, response: JsonDict, use_state_after: bool) -> list[str]:
"""Extract the list of member state keys from the test room in a sync
response."""
return [
e["state_key"]
for e in self._state_events(response, use_state_after)
if e["type"] == EventTypes.Member
]
@parameterized.expand([("state", False), ("state_after", True)])
def test_member_sent_once_when_acknowledged(
self, _: str, use_state_after: bool
) -> None:
"""A lazy-loaded member is sent the first time it is needed and left
out once the client has synced past the response that carried it."""
t0 = self._sync(use_state_after)["next_batch"]
self.helper.send(self.room_id, body="one", tok=self.bob_tok)
response = self._sync(use_state_after, t0)
self.assertEqual(self._members_in_state(response, use_state_after), [self.bob])
t1 = response["next_batch"]
self.helper.send(self.room_id, body="two", tok=self.bob_tok)
response = self._sync(use_state_after, t1)
self.assertEqual(self._members_in_state(response, use_state_after), [])
@parameterized.expand([("state", False), ("state_after", True)])
def test_member_not_resent_on_resync_from_token_that_carried_it(
self, _: str, use_state_after: bool
) -> None:
"""A client that resumes from an older token is only sent again the
members carried by the responses after that token.
A member sent in the response that ended at the resumed-from token was
processed by the client before it saved that token, so it must not be
sent again. matrix-js-sdk saves its sync store every few minutes and
resumes from the saved token on reload, so this is the common case.
"""
t0 = self._sync(use_state_after)["next_batch"]
# bob's membership is carried by the response that ends at `t1`.
self.helper.send(self.room_id, body="one", tok=self.bob_tok)
response = self._sync(use_state_after, t0)
self.assertEqual(self._members_in_state(response, use_state_after), [self.bob])
t1 = response["next_batch"]
# The client processes further responses, then reloads from `t1`.
self.helper.send(self.room_id, body="two", tok=self.bob_tok)
self._sync(use_state_after, t1)
self.helper.send(self.room_id, body="three", tok=self.bob_tok)
response = self._sync(use_state_after, t1)
self.assertEqual(
self._members_in_state(response, use_state_after),
[],
f"bob's membership re-sent on resume from the token that carried "
f"it: {response}",
)
@parameterized.expand([("state", False), ("state_after", True)])
def test_member_resent_on_retry_from_same_token(
self, _: str, use_state_after: bool
) -> None:
"""A client that gives up on a slow request and retries it must get the
lazy-loaded members again. It never processed the first response."""
t0 = self._sync(use_state_after)["next_batch"]
self.helper.send(self.room_id, body="hello", tok=self.bob_tok)
for attempt in range(2):
response = self._sync(use_state_after, t0)
self.assertEqual(
self._members_in_state(response, use_state_after),
[self.bob],
f"bob's membership missing on attempt {attempt}: {response}",
)
def test_membership_change_resent_on_resync_from_older_token(self) -> None:
"""A membership change must be in `state_after` every time the sync
window covers it, even if it has been sent before.
Clients do not persist every `since` token. matrix-js-sdk saves its
sync store every few minutes, so after a reload it resumes from a token
older than the one the change was first delivered on. A `state_after`
client never applies state from the timeline, so if the change is left
out of `state_after` it keeps the stale membership until it clears its
cache. A kicked user stays shown as joined.
"""
t0 = self._sync(use_state_after=True)["next_batch"]
self.helper.send(self.room_id, body="hello", tok=self.bob_tok)
t1 = self._sync(use_state_after=True, since=t0)["next_batch"]
kick = self.helper.change_membership(
self.room_id, self.alice, self.bob, "leave", tok=self.alice_tok
)
# The live client sees the kick.
response = self._sync(use_state_after=True, since=t1)
self.assertIn(kick["event_id"], self._state_ids(response, use_state_after=True))
# The client reloads from the older persisted token.
response = self._sync(use_state_after=True, since=t0)
self.assertIn(kick["event_id"], self._timeline_ids(response))
self.assertIn(
kick["event_id"],
self._state_ids(response, use_state_after=True),
f"membership change missing from state_after on re-sync from an "
f"older token: {response}",
)
def test_gappy_membership_change_resent_on_resync_from_older_token(
self,
) -> None:
"""The same for `state`. A membership change that falls in the gap of
a limited timeline is only conveyed by `state`, so it must be there
every time the sync window covers it."""
t0 = self._sync(use_state_after=False, timeline_limit=1)["next_batch"]
self.helper.send(self.room_id, body="hello", tok=self.bob_tok)
t1 = self._sync(use_state_after=False, since=t0, timeline_limit=1)["next_batch"]
kick = self.helper.change_membership(
self.room_id, self.alice, self.bob, "leave", tok=self.alice_tok
)
# A later message, so that the kick falls into the gap of the
# one-event timeline.
self.helper.send(self.room_id, body="after", tok=self.alice_tok)
# The live client sees the kick in `state`.
response = self._sync(use_state_after=False, since=t1, timeline_limit=1)
self.assertTrue(self._room(response)["timeline"]["limited"])
self.assertNotIn(kick["event_id"], self._timeline_ids(response))
self.assertIn(
kick["event_id"], self._state_ids(response, use_state_after=False)
)
# The client reloads from the older persisted token.
response = self._sync(use_state_after=False, since=t0, timeline_limit=1)
self.assertNotIn(kick["event_id"], self._timeline_ids(response))
self.assertIn(
kick["event_id"],
self._state_ids(response, use_state_after=False),
f"membership change missing from state on re-sync from an older "
f"token: {response}",
)
@parameterized.expand([("state", False), ("state_after", True)])
def test_member_not_resent_after_empty_long_poll_wakeup(
self, _: str, use_state_after: bool
) -> None:
"""A long-poll that wakes up with nothing to send keeps waiting. The
response it eventually returns must still leave out the members the
client already has.
The notifier calls back into the sync handler on every wakeup within
one request. An empty result is not a response the client lost.
"""
not_types = ["m.spurious"]
t0 = self._sync(use_state_after, timeline_not_types=not_types)["next_batch"]
self.helper.send(self.room_id, body="one", tok=self.bob_tok)
response = self._sync(use_state_after, t0, timeline_not_types=not_types)
self.assertEqual(self._members_in_state(response, use_state_after), [self.bob])
t1 = response["next_batch"]
channel = self._sync_request(
use_state_after,
t1,
timeline_not_types=not_types,
timeout_ms=30000,
await_result=False,
)
# Wakes the long-poll, but the filter drops the event, so the result is
# empty and the notifier waits again.
self.helper.send_event(
self.room_id, type="m.spurious", content={}, tok=self.bob_tok
)
self.assertFalse(channel.is_finished())
self.helper.send(self.room_id, body="two", tok=self.bob_tok)
channel.await_result()
self.assertEqual(channel.code, 200, channel.result)
response = channel.json_body
self.assertEqual(len(self._timeline_ids(response)), 1)
self.assertEqual(self._members_in_state(response, use_state_after), [])
@parameterized.expand([("state", False), ("state_after", True)])
def test_membership_change_sent_when_cache_claims_it_was(
self, _: str, use_state_after: bool
) -> None:
"""A membership change since `since` is sent even when the cache says
the client already has it.
The cache only decides whether to leave out the memberships fetched for
timeline senders. It can be wrong: a retry of a request can run while
the original is still being built, and the original then marks members
the retry has to send. A change must never depend on it.
"""
t0 = self._sync(use_state_after, timeline_limit=1)["next_batch"]
kick = self.helper.change_membership(
self.room_id, self.alice, self.bob, "leave", tok=self.alice_tok
)
# A later message, so that the kick falls into the gap of the
# one-event timeline and only `state` can carry it.
self.helper.send(self.room_id, body="after the kick", tok=self.alice_tok)
# Claim the kick was already sent in the response that ended at `t0`.
# Syncing from `t0` does not forget this, as the client has acknowledged
# that response.
t0_token = self.get_success(
StreamToken.from_string(self.hs.get_datastores().main, t0)
)
cache = self.hs.get_sync_handler().get_lazy_loaded_members_cache(
(self.alice, self.alice_device)
)
cache.mark_sent(self.bob, kick["event_id"], t0_token)
response = self._sync(use_state_after, t0, timeline_limit=1)
self.assertNotIn(kick["event_id"], self._timeline_ids(response))
self.assertIn(kick["event_id"], self._state_ids(response, use_state_after))