mirror of
https://github.com/element-hq/synapse.git
synced 2026-10-23 10:53:31 +00:00
Merge remote-tracking branch 'origin/develop' into erikj/rust_event_serialization
This commit is contained in:
commit
15a1eea8b6
113 files changed
+3080
-626
No files matched your search
@@ -39,7 +39,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout synapse codebase
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||
with:
|
||||
path: synapse
|
||||
|
||||
@@ -58,7 +58,7 @@ jobs:
|
||||
# We use `poetry` in `complement.sh`
|
||||
- uses: matrix-org/setup-python-poetry@5bbf6603c5c930615ec8a29f1b5d7d258d905aa4 # v2.0.0
|
||||
with:
|
||||
poetry-version: "2.2.1"
|
||||
poetry-version: "2.4.1"
|
||||
# Matches the `path` where we checkout Synapse above
|
||||
working-directory: "synapse"
|
||||
|
||||
@@ -76,7 +76,7 @@ jobs:
|
||||
run: |
|
||||
set -x
|
||||
DEBIAN_FRONTEND=noninteractive sudo apt-get install -yqq python3 pipx
|
||||
pipx install poetry==2.2.1
|
||||
pipx install poetry==2.4.1
|
||||
|
||||
poetry remove -n twisted
|
||||
poetry add -n --extras tls git+https://github.com/twisted/twisted.git#trunk
|
||||
@@ -92,13 +92,24 @@ jobs:
|
||||
# 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.
|
||||
# tee /tmp/gotest-sanity-check-complement.log: We tee the raw JSON to a
|
||||
# file so we can re-process it later for better formatting with gotestfmt
|
||||
# (and upload it as an artifact).
|
||||
#
|
||||
# We deliberately do not dump the raw JSON to the terminal as it is huge
|
||||
# and unreadable. Instead we pipe it through `jq` to print a compact,
|
||||
# real-time progress view: for each `go test -json` event we print only
|
||||
# completed tests (`pass`/`skip`/`fail`, including subtests), dropping the
|
||||
# noisy `run`/`output` events and package-level lines. The full detail
|
||||
# lives in the raw log we tee to the file above. Non-JSON lines (e.g. the
|
||||
# image build output and `set -x` traces that `go test -json` does not
|
||||
# wrap) are passed through unchanged rather than parsed, so we still see
|
||||
# them and the filter never aborts the pipeline.
|
||||
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
|
||||
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 \
|
||||
| jq -rR --unbuffered '. as $raw | (try fromjson catch $raw) | if type == "object" then (select(.Action=="pass" or .Action=="fail" or .Action=="skip") | select(.Test) | "\(.Action|ascii_upcase) \(.Test) \(.Elapsed)s") else $raw end'
|
||||
shell: bash
|
||||
env:
|
||||
POSTGRES: ${{ (matrix.database == 'Postgres') && 1 || '' }}
|
||||
@@ -121,13 +132,16 @@ jobs:
|
||||
# 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.
|
||||
# tee /tmp/gotest-complement.log: We tee the raw JSON to a file so we can
|
||||
# re-process it later for better formatting with gotestfmt (and upload it
|
||||
# as an artifact), and pipe it through `jq` for a compact, real-time
|
||||
# progress view. See the sanity check step above for details on the jq
|
||||
# filter.
|
||||
run: |
|
||||
set -o pipefail
|
||||
COMPLEMENT_DIR=`pwd`/complement synapse/scripts-dev/complement.sh -p 1 -json 2>&1 | tee /tmp/gotest-complement.log
|
||||
COMPLEMENT_DIR=`pwd`/complement synapse/scripts-dev/complement.sh -p 1 -json 2>&1 \
|
||||
| tee /tmp/gotest-complement.log \
|
||||
| jq -rR --unbuffered '. as $raw | (try fromjson catch $raw) | if type == "object" then (select(.Action=="pass" or .Action=="fail" or .Action=="skip") | select(.Test) | "\(.Action|ascii_upcase) \(.Test) \(.Elapsed)s") else $raw end'
|
||||
shell: bash
|
||||
env:
|
||||
POSTGRES: ${{ (matrix.database == 'Postgres') && 1 || '' }}
|
||||
@@ -151,13 +165,16 @@ jobs:
|
||||
# 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.
|
||||
# tee /tmp/gotest-in-repo-complement.log: We tee the raw JSON to a file so
|
||||
# we can re-process it later for better formatting with gotestfmt (and
|
||||
# upload it as an artifact), and pipe it through `jq` for a compact,
|
||||
# real-time progress view. See the sanity check step above for details on
|
||||
# the jq filter.
|
||||
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
|
||||
COMPLEMENT_DIR=`pwd`/complement synapse/scripts-dev/complement.sh --in-repo -p 1 -json 2>&1 \
|
||||
| tee /tmp/gotest-in-repo-complement.log \
|
||||
| jq -rR --unbuffered '. as $raw | (try fromjson catch $raw) | if type == "object" then (select(.Action=="pass" or .Action=="fail" or .Action=="skip") | select(.Test) | "\(.Action|ascii_upcase) \(.Test) \(.Elapsed)s") else $raw end'
|
||||
shell: bash
|
||||
env:
|
||||
POSTGRES: ${{ (matrix.database == 'Postgres') && 1 || '' }}
|
||||
@@ -174,3 +191,18 @@ jobs:
|
||||
# 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"
|
||||
|
||||
- name: Upload Complement logs
|
||||
# Always upload the logs (as artifacts) if we attempted to run the
|
||||
# Complement tests, so we can debug failures without scrolling through
|
||||
# the (very large) raw output in the build log.
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
if: ${{ always() }}
|
||||
with:
|
||||
# The matrix values keep the name unique across the three matrix
|
||||
# combinations (upload-artifact rejects duplicate names within a run).
|
||||
name: Complement Logs - ${{ job.status }} - (${{ matrix.arrangement }}, ${{ matrix.database }})
|
||||
path: |
|
||||
/tmp/gotest-sanity-check-complement.log
|
||||
/tmp/gotest-complement.log
|
||||
/tmp/gotest-in-repo-complement.log
|
||||
@@ -28,10 +28,10 @@ jobs:
|
||||
steps:
|
||||
- name: Set up Docker Buildx
|
||||
id: buildx
|
||||
uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4.0.0
|
||||
uses: docker/setup-buildx-action@d7f5e7f509e45cec5c76c4d5afdd7de93d0b3df5 # v4.1.0
|
||||
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||
|
||||
- name: Extract version from pyproject.toml
|
||||
# Note: explicitly requesting bash will mean bash is invoked with `-eo pipefail`, see
|
||||
@@ -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@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0
|
||||
uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4.2.0
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
|
||||
- name: Log in to GHCR
|
||||
uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0
|
||||
uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4.2.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@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0
|
||||
uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4.2.0
|
||||
with:
|
||||
registry: oci-push.vpn.infra.element.io
|
||||
username: ${{ steps.import-secrets.outputs.OCI_USERNAME }}
|
||||
@@ -87,7 +87,7 @@ jobs:
|
||||
|
||||
- name: Build and push by digest
|
||||
id: build
|
||||
uses: docker/build-push-action@bcafcacb16a39f128d818304e6c9c0c18556b85f # v7.1.0
|
||||
uses: docker/build-push-action@f9f3042f7e2789586610d6e8b85c8f03e5195baf # v7.2.0
|
||||
with:
|
||||
push: true
|
||||
labels: |
|
||||
@@ -136,14 +136,14 @@ jobs:
|
||||
merge-multiple: true
|
||||
|
||||
- name: Log in to DockerHub
|
||||
uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0
|
||||
uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4.2.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@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0
|
||||
uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4.2.0
|
||||
if: ${{ startsWith(matrix.repository, 'ghcr.io') }}
|
||||
with:
|
||||
registry: ghcr.io
|
||||
@@ -176,20 +176,20 @@ jobs:
|
||||
services/backend-repositories/secret/data/oci.element.io password | OCI_PASSWORD ;
|
||||
|
||||
- name: Login to Element OCI Registry
|
||||
uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0
|
||||
uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4.2.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@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4.0.0
|
||||
uses: docker/setup-buildx-action@d7f5e7f509e45cec5c76c4d5afdd7de93d0b3df5 # v4.1.0
|
||||
|
||||
- name: Install Cosign
|
||||
uses: sigstore/cosign-installer@6f9f17788090df1f26f669e9d70d6ae9567deba6 # v4.1.2
|
||||
|
||||
- name: Calculate docker image tag
|
||||
uses: docker/metadata-action@030e881283bb7a6894de51c315a6bfe6a94e05cf # v6.0.0
|
||||
uses: docker/metadata-action@80c7e94dd9b9319bd5eb7a0e0fe9291e23a2a2e9 # v6.1.0
|
||||
with:
|
||||
images: ${{ matrix.repository }}
|
||||
flavor: |
|
||||
|
||||
@@ -13,7 +13,7 @@ jobs:
|
||||
name: GitHub Pages
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||
with:
|
||||
# Fetch all history so that the schema_versions script works.
|
||||
fetch-depth: 0
|
||||
@@ -50,7 +50,7 @@ jobs:
|
||||
name: Check links in documentation
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||
|
||||
- name: Setup mdbook
|
||||
uses: peaceiris/actions-mdbook@ee69d230fe19748b7abf22df32acaa93833fad08 # v2.0.0
|
||||
|
||||
@@ -50,7 +50,7 @@ jobs:
|
||||
needs:
|
||||
- pre
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||
with:
|
||||
# Fetch all history so that the schema_versions script works.
|
||||
fetch-depth: 0
|
||||
@@ -92,7 +92,7 @@ jobs:
|
||||
|
||||
# Deploy to the target directory.
|
||||
- name: Deploy to gh pages
|
||||
uses: peaceiris/actions-gh-pages@4f9cc6602d3f66b9c108549d475ec49e8ef4d45e # v4.0.0
|
||||
uses: peaceiris/actions-gh-pages@84c30a85c19949d7eee79c4ff27748b70285e453 # v4.1.0
|
||||
with:
|
||||
github_token: ${{ secrets.GITHUB_TOKEN }}
|
||||
publish_dir: ./book
|
||||
|
||||
@@ -18,7 +18,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||
|
||||
- name: Install Rust
|
||||
uses: dtolnay/rust-toolchain@e97e2d8cc328f1b50210efc529dca0028893a2d9 # master
|
||||
@@ -31,7 +31,7 @@ jobs:
|
||||
uses: matrix-org/setup-python-poetry@5bbf6603c5c930615ec8a29f1b5d7d258d905aa4 # v2.0.0
|
||||
with:
|
||||
install-project: "false"
|
||||
poetry-version: "2.2.1"
|
||||
poetry-version: "2.4.1"
|
||||
|
||||
- name: Run ruff check
|
||||
continue-on-error: true
|
||||
|
||||
@@ -42,7 +42,7 @@ jobs:
|
||||
if: needs.check_repo.outputs.should_run_workflow == 'true'
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||
- name: Install Rust
|
||||
uses: dtolnay/rust-toolchain@e97e2d8cc328f1b50210efc529dca0028893a2d9 # master
|
||||
with:
|
||||
@@ -54,7 +54,7 @@ jobs:
|
||||
- uses: matrix-org/setup-python-poetry@5bbf6603c5c930615ec8a29f1b5d7d258d905aa4 # v2.0.0
|
||||
with:
|
||||
python-version: "3.x"
|
||||
poetry-version: "2.2.1"
|
||||
poetry-version: "2.4.1"
|
||||
extras: "all"
|
||||
# Dump installed versions for debugging.
|
||||
- run: poetry run pip list > before.txt
|
||||
@@ -77,7 +77,7 @@ jobs:
|
||||
postgres-version: "14"
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||
|
||||
- name: Install Rust
|
||||
uses: dtolnay/rust-toolchain@e97e2d8cc328f1b50210efc529dca0028893a2d9 # master
|
||||
@@ -151,7 +151,7 @@ jobs:
|
||||
BLACKLIST: ${{ matrix.workers && 'synapse-blacklist-with-workers' }}
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||
|
||||
- name: Install Rust
|
||||
uses: dtolnay/rust-toolchain@e97e2d8cc328f1b50210efc529dca0028893a2d9 # master
|
||||
@@ -201,7 +201,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||
- uses: JasonEtco/create-an-issue@1b14a70e4d8dc185e5cc76d3bec9eab20257b2c5 # v2.9.2
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
@@ -16,7 +16,7 @@ jobs:
|
||||
name: "Check locked dependencies have sdists"
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||
- uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
|
||||
with:
|
||||
python-version: '3.x'
|
||||
|
||||
@@ -33,33 +33,33 @@ jobs:
|
||||
packages: write
|
||||
steps:
|
||||
- name: Checkout specific branch (debug build)
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||
if: github.event_name == 'workflow_dispatch'
|
||||
with:
|
||||
ref: ${{ inputs.branch }}
|
||||
- name: Checkout clean copy of develop (scheduled build)
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||
if: github.event_name == 'schedule'
|
||||
with:
|
||||
ref: develop
|
||||
- name: Checkout clean copy of master (on-push)
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||
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"
|
||||
poetry-version: "2.4.1"
|
||||
- name: Login to registry
|
||||
uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0
|
||||
uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4.2.0
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
- name: Work out labels for complement image
|
||||
id: meta
|
||||
uses: docker/metadata-action@030e881283bb7a6894de51c315a6bfe6a94e05cf # v6.0.0
|
||||
uses: docker/metadata-action@80c7e94dd9b9319bd5eb7a0e0fe9291e23a2a2e9 # v6.1.0
|
||||
with:
|
||||
images: ghcr.io/${{ github.repository }}/complement-synapse
|
||||
tags: |
|
||||
|
||||
@@ -27,7 +27,7 @@ jobs:
|
||||
name: "Calculate list of debian distros"
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||
- uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
|
||||
with:
|
||||
python-version: "3.x"
|
||||
@@ -50,18 +50,24 @@ jobs:
|
||||
name: "Build .deb packages"
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
# Prevent all Debian package builds from being cancelled
|
||||
# when one of them fails. Especially helpful when things are
|
||||
# flaky during a release.
|
||||
#
|
||||
# `fail-fast` defaults to true and applies to the entire test matrix
|
||||
fail-fast: false
|
||||
matrix:
|
||||
distro: ${{ fromJson(needs.get-distros.outputs.distros) }}
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||
with:
|
||||
path: src
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
id: buildx
|
||||
uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4.0.0
|
||||
uses: docker/setup-buildx-action@d7f5e7f509e45cec5c76c4d5afdd7de93d0b3df5 # v4.1.0
|
||||
|
||||
- name: Set up docker layer caching
|
||||
uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
|
||||
@@ -123,7 +129,7 @@ jobs:
|
||||
os: "ubuntu-24.04-arm"
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||
|
||||
- uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
|
||||
with:
|
||||
@@ -161,7 +167,7 @@ jobs:
|
||||
if: ${{ !startsWith(github.ref, 'refs/pull/') }}
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||
- uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
|
||||
with:
|
||||
python-version: "3.10"
|
||||
|
||||
@@ -14,7 +14,7 @@ jobs:
|
||||
name: Ensure Synapse config schema is valid
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||
- uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
|
||||
with:
|
||||
python-version: "3.x"
|
||||
@@ -40,7 +40,7 @@ jobs:
|
||||
name: Ensure generated documentation is up-to-date
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||
- uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
|
||||
with:
|
||||
python-version: "3.x"
|
||||
|
||||
+32
-30
@@ -60,8 +60,10 @@ jobs:
|
||||
- 'poetry.lock'
|
||||
- 'docker/**'
|
||||
- '.ci/**'
|
||||
- 'scripts-dev/complement.sh'
|
||||
- '.github/workflows/tests.yml'
|
||||
- 'scripts-dev/complement.sh'
|
||||
- '.github/workflows/complement_tests.yml'
|
||||
- 'complement/**'
|
||||
|
||||
linting:
|
||||
- 'synapse/**'
|
||||
@@ -86,7 +88,7 @@ jobs:
|
||||
if: ${{ needs.changes.outputs.linting == 'true' }}
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||
- name: Install Rust
|
||||
uses: dtolnay/rust-toolchain@e97e2d8cc328f1b50210efc529dca0028893a2d9 # master
|
||||
with:
|
||||
@@ -95,7 +97,7 @@ jobs:
|
||||
- uses: matrix-org/setup-python-poetry@5bbf6603c5c930615ec8a29f1b5d7d258d905aa4 # v2.0.0
|
||||
with:
|
||||
python-version: "3.x"
|
||||
poetry-version: "2.2.1"
|
||||
poetry-version: "2.4.1"
|
||||
extras: "all"
|
||||
- run: poetry run scripts-dev/generate_sample_config.sh --check
|
||||
- run: poetry run scripts-dev/config-lint.sh
|
||||
@@ -106,7 +108,7 @@ jobs:
|
||||
if: ${{ needs.changes.outputs.linting == 'true' }}
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||
- uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
|
||||
with:
|
||||
python-version: "3.x"
|
||||
@@ -116,7 +118,7 @@ jobs:
|
||||
check-lockfile:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||
- uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
|
||||
with:
|
||||
python-version: "3.x"
|
||||
@@ -129,12 +131,12 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||
|
||||
- name: Setup Poetry
|
||||
uses: matrix-org/setup-python-poetry@5bbf6603c5c930615ec8a29f1b5d7d258d905aa4 # v2.0.0
|
||||
with:
|
||||
poetry-version: "2.2.1"
|
||||
poetry-version: "2.4.1"
|
||||
install-project: "false"
|
||||
|
||||
- name: Run ruff check
|
||||
@@ -151,7 +153,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||
|
||||
- name: Install Rust
|
||||
uses: dtolnay/rust-toolchain@e97e2d8cc328f1b50210efc529dca0028893a2d9 # master
|
||||
@@ -169,7 +171,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.2.1"
|
||||
poetry-version: "2.4.1"
|
||||
|
||||
# Cribbed from
|
||||
# https://github.com/AustinScola/mypy-cache-github-action/blob/85ea4f2972abed39b33bd02c36e341b28ca59213/src/restore.ts#L10-L17
|
||||
@@ -187,7 +189,7 @@ jobs:
|
||||
lint-crlf:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||
- name: Check line endings
|
||||
run: scripts-dev/check_line_terminators.sh
|
||||
|
||||
@@ -196,7 +198,7 @@ jobs:
|
||||
if: ${{ github.event_name == 'pull_request' && (github.base_ref == 'develop' || contains(github.base_ref, 'release-')) && github.event.pull_request.user.login != 'dependabot[bot]' }}
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||
with:
|
||||
ref: ${{ github.event.pull_request.head.sha }}
|
||||
fetch-depth: 0
|
||||
@@ -214,7 +216,7 @@ jobs:
|
||||
if: ${{ needs.changes.outputs.rust == 'true' }}
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||
|
||||
- name: Install Rust
|
||||
uses: dtolnay/rust-toolchain@e97e2d8cc328f1b50210efc529dca0028893a2d9 # master
|
||||
@@ -233,7 +235,7 @@ jobs:
|
||||
if: ${{ needs.changes.outputs.rust == 'true' }}
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||
|
||||
- name: Install Rust
|
||||
uses: dtolnay/rust-toolchain@e97e2d8cc328f1b50210efc529dca0028893a2d9 # master
|
||||
@@ -251,7 +253,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||
|
||||
- name: Install Rust
|
||||
uses: dtolnay/rust-toolchain@e97e2d8cc328f1b50210efc529dca0028893a2d9 # master
|
||||
@@ -265,7 +267,7 @@ jobs:
|
||||
# Install like a normal project from source with all optional dependencies
|
||||
extras: all
|
||||
install-project: "true"
|
||||
poetry-version: "2.2.1"
|
||||
poetry-version: "2.4.1"
|
||||
|
||||
- name: Ensure `Cargo.lock` is up to date (no stray changes after install)
|
||||
# The `::error::` syntax is using GitHub Actions' error annotations, see
|
||||
@@ -287,7 +289,7 @@ jobs:
|
||||
if: ${{ needs.changes.outputs.rust == 'true' }}
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||
|
||||
- name: Install Rust
|
||||
uses: dtolnay/rust-toolchain@e97e2d8cc328f1b50210efc529dca0028893a2d9 # master
|
||||
@@ -307,7 +309,7 @@ jobs:
|
||||
needs: changes
|
||||
if: ${{ needs.changes.outputs.linting_readme == 'true' }}
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||
- uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
|
||||
with:
|
||||
python-version: "3.x"
|
||||
@@ -354,7 +356,7 @@ jobs:
|
||||
needs: linting-done
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||
- uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
|
||||
with:
|
||||
python-version: "3.x"
|
||||
@@ -375,7 +377,7 @@ jobs:
|
||||
job: ${{ fromJson(needs.calculate-test-jobs.outputs.trial_test_matrix) }}
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||
- run: sudo apt-get -qq install xmlsec1
|
||||
- name: Set up PostgreSQL ${{ matrix.job.postgres-version }}
|
||||
if: ${{ matrix.job.postgres-version }}
|
||||
@@ -398,7 +400,7 @@ jobs:
|
||||
- uses: matrix-org/setup-python-poetry@5bbf6603c5c930615ec8a29f1b5d7d258d905aa4 # v2.0.0
|
||||
with:
|
||||
python-version: ${{ matrix.job.python-version }}
|
||||
poetry-version: "2.2.1"
|
||||
poetry-version: "2.4.1"
|
||||
extras: ${{ matrix.job.extras }}
|
||||
- name: Await PostgreSQL
|
||||
if: ${{ matrix.job.postgres-version }}
|
||||
@@ -431,7 +433,7 @@ jobs:
|
||||
- changes
|
||||
runs-on: ubuntu-22.04
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||
|
||||
- name: Install Rust
|
||||
uses: dtolnay/rust-toolchain@e97e2d8cc328f1b50210efc529dca0028893a2d9 # master
|
||||
@@ -494,13 +496,13 @@ jobs:
|
||||
extras: ["all"]
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||
# Install libs necessary for PyPy to build binary wheels for dependencies
|
||||
- run: sudo apt-get -qq install xmlsec1 libxml2-dev libxslt-dev
|
||||
- uses: matrix-org/setup-python-poetry@5bbf6603c5c930615ec8a29f1b5d7d258d905aa4 # v2.0.0
|
||||
with:
|
||||
python-version: ${{ matrix.python-version }}
|
||||
poetry-version: "2.2.1"
|
||||
poetry-version: "2.4.1"
|
||||
extras: ${{ matrix.extras }}
|
||||
- run: poetry run trial --jobs=2 tests
|
||||
- name: Dump logs
|
||||
@@ -544,7 +546,7 @@ jobs:
|
||||
job: ${{ fromJson(needs.calculate-test-jobs.outputs.sytest_test_matrix) }}
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||
- name: Prepare test blacklist
|
||||
run: cat sytest-blacklist .ci/worker-blacklist > synapse-blacklist-with-workers
|
||||
|
||||
@@ -591,11 +593,11 @@ jobs:
|
||||
--health-retries 5
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||
- run: sudo apt-get -qq install xmlsec1 postgresql-client
|
||||
- uses: matrix-org/setup-python-poetry@5bbf6603c5c930615ec8a29f1b5d7d258d905aa4 # v2.0.0
|
||||
with:
|
||||
poetry-version: "2.2.1"
|
||||
poetry-version: "2.4.1"
|
||||
extras: "postgres"
|
||||
- run: .ci/scripts/test_export_data_command.sh
|
||||
env:
|
||||
@@ -634,7 +636,7 @@ jobs:
|
||||
--health-retries 5
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||
- name: Add PostgreSQL apt repository
|
||||
# We need a version of pg_dump that can handle the version of
|
||||
# PostgreSQL being tested against. The Ubuntu package repository lags
|
||||
@@ -648,7 +650,7 @@ jobs:
|
||||
- uses: matrix-org/setup-python-poetry@5bbf6603c5c930615ec8a29f1b5d7d258d905aa4 # v2.0.0
|
||||
with:
|
||||
python-version: ${{ matrix.python-version }}
|
||||
poetry-version: "2.2.1"
|
||||
poetry-version: "2.4.1"
|
||||
extras: "postgres"
|
||||
- run: .ci/scripts/test_synapse_port_db.sh
|
||||
id: run_tester_script
|
||||
@@ -682,7 +684,7 @@ jobs:
|
||||
- changes
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||
|
||||
- name: Install Rust
|
||||
uses: dtolnay/rust-toolchain@e97e2d8cc328f1b50210efc529dca0028893a2d9 # master
|
||||
@@ -702,7 +704,7 @@ jobs:
|
||||
- changes
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||
|
||||
- name: Install Rust
|
||||
uses: dtolnay/rust-toolchain@e97e2d8cc328f1b50210efc529dca0028893a2d9 # master
|
||||
|
||||
@@ -22,7 +22,7 @@ jobs:
|
||||
# This field is case-sensitive.
|
||||
TARGET_STATUS: Needs info
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||
with:
|
||||
# Only clone the script file we care about, instead of the whole repo.
|
||||
sparse-checkout: .ci/scripts/triage_labelled_issue.sh
|
||||
|
||||
@@ -42,7 +42,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||
|
||||
- name: Install Rust
|
||||
uses: dtolnay/rust-toolchain@e97e2d8cc328f1b50210efc529dca0028893a2d9 # master
|
||||
@@ -54,7 +54,7 @@ jobs:
|
||||
with:
|
||||
python-version: "3.x"
|
||||
extras: "all"
|
||||
poetry-version: "2.2.1"
|
||||
poetry-version: "2.4.1"
|
||||
- run: |
|
||||
poetry remove twisted
|
||||
poetry add --extras tls git+https://github.com/twisted/twisted.git#${{ inputs.twisted_ref || 'trunk' }}
|
||||
@@ -69,7 +69,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||
- run: sudo apt-get -qq install xmlsec1
|
||||
|
||||
- name: Install Rust
|
||||
@@ -82,7 +82,7 @@ jobs:
|
||||
with:
|
||||
python-version: "3.x"
|
||||
extras: "all test"
|
||||
poetry-version: "2.2.1"
|
||||
poetry-version: "2.4.1"
|
||||
- run: |
|
||||
poetry remove twisted
|
||||
poetry add --extras tls git+https://github.com/twisted/twisted.git#trunk
|
||||
@@ -115,7 +115,7 @@ jobs:
|
||||
- ${{ github.workspace }}:/src
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||
|
||||
- name: Install Rust
|
||||
uses: dtolnay/rust-toolchain@e97e2d8cc328f1b50210efc529dca0028893a2d9 # master
|
||||
@@ -172,7 +172,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||
- uses: JasonEtco/create-an-issue@1b14a70e4d8dc185e5cc76d3bec9eab20257b2c5 # v2.9.2
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
@@ -53,6 +53,7 @@ __pycache__/
|
||||
!/.coveragerc
|
||||
/.coverage*
|
||||
/.mypy_cache/
|
||||
/.ruff_cache/
|
||||
/.tox
|
||||
/.tox-pg-container
|
||||
/build/
|
||||
|
||||
+11
@@ -1,3 +1,14 @@
|
||||
# Synapse 1.155.0 (2026-06-16)
|
||||
|
||||
## End of Life of Debian 12 Bookworm
|
||||
|
||||
The next version of Synapse will not include Debian packages for Debian 12 Bookworm
|
||||
as it reached end of life on the 10th of June 2026.
|
||||
|
||||
## Internal Changes
|
||||
|
||||
- When building releases, don't cancel Debian package builds when one of them fails. ([\#19842](https://github.com/element-hq/synapse/issues/19842))
|
||||
|
||||
# Synapse 1.155.0rc1 (2026-06-09)
|
||||
|
||||
## Bugfixes
|
||||
|
||||
Generated
+6
-6
@@ -373,9 +373,9 @@ checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70"
|
||||
|
||||
[[package]]
|
||||
name = "http"
|
||||
version = "1.4.0"
|
||||
version = "1.4.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e3ba2a386d7f85a81f119ad7498ebe444d2e22c2af0b86b069416ace48b3311a"
|
||||
checksum = "6970f50e31d6fc17d3fa27329444bfa74e196cf62e95052a3f6fee181dba6425"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"itoa",
|
||||
@@ -707,9 +707,9 @@ checksum = "241eaef5fd12c88705a01fc1066c48c4b36e0dd4377dcdc7ec3942cea7a69956"
|
||||
|
||||
[[package]]
|
||||
name = "log"
|
||||
version = "0.4.29"
|
||||
version = "0.4.32"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897"
|
||||
checksum = "953f07c43838f8e6f9758cab68bf5bed85465e7587ebe0b823f1bcd81978ad3a"
|
||||
|
||||
[[package]]
|
||||
name = "lru-slab"
|
||||
@@ -1208,9 +1208,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "serde_json"
|
||||
version = "1.0.149"
|
||||
version = "1.0.150"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86"
|
||||
checksum = "e8014e44b4736ed0538adeecded0fce2a272f22dc9578a7eb6b2d9993c74cfb9"
|
||||
dependencies = [
|
||||
"itoa",
|
||||
"memchr",
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
Provide remote servers a way to find out about an event created during the remote join handshake. Contributed by @FrenchGithubUser and @jason-famedly @ Famedly.
|
||||
@@ -0,0 +1 @@
|
||||
Expose [MSC4354 Sticky Events](https://github.com/matrix-org/matrix-spec-proposals/pull/4354) over [MSC4186 (Simplified) Sliding Sync](https://github.com/matrix-org/matrix-spec-proposals/pull/4186).
|
||||
@@ -0,0 +1 @@
|
||||
Update `auto_join_rooms` config documentation to cover requirements for auto-joining invite-only rooms.
|
||||
@@ -0,0 +1 @@
|
||||
Refactor `get_user_which_could_invite` logic to reuse `get_users_which_can_issue_invite`. Contributed by Noah Markert.
|
||||
@@ -0,0 +1 @@
|
||||
Stabilize support for sending ephemeral events to application services, as per [MSC2409](https://github.com/matrix-org/matrix-spec-proposals/pull/2409). Contributed by @jason-famedly @ Famedly.
|
||||
@@ -0,0 +1,2 @@
|
||||
Include `allowed_room_ids` in the `/summary` client-server API response for rooms with restricted join rules, as required by Matrix 1.15.
|
||||
Contributed by @FrenchGithubUser @Famedly.
|
||||
@@ -0,0 +1 @@
|
||||
Fix a long-standing bug where the badge notification count for a room could become permanently inflated if a read receipt was sent before the room's notification counts were first summarised.
|
||||
@@ -0,0 +1 @@
|
||||
Add stable endpoint for [MSC3266: Room summary API](https://github.com/matrix-org/matrix-spec-proposals/pull/3266) into worker docs. Contributed by @olmari.
|
||||
@@ -0,0 +1 @@
|
||||
[MSC4140: Cancellable delayed events](https://github.com/matrix-org/matrix-spec-proposals/pull/4140): Allow authentication on delayed event management endpoints (such as `/restart`) to bypass ratelimits for unauthenticated requests based on the client IP address.
|
||||
@@ -0,0 +1 @@
|
||||
Fix startup listener logging to report the actual bound TCP port, so listeners configured with port `0` no longer log `Synapse now listening on TCP port 0`.
|
||||
@@ -0,0 +1 @@
|
||||
Tweak wording of Rust crate dependency update policy.
|
||||
@@ -0,0 +1 @@
|
||||
Fix notification counts being inflated after a `/purge_history` when notifications had already been rotated into the summary table.
|
||||
@@ -0,0 +1 @@
|
||||
Upload Complement test logs as CI artifacts instead of printing the raw output to the build log.
|
||||
@@ -0,0 +1 @@
|
||||
Fix release script considering any workflow completion as successful.
|
||||
@@ -0,0 +1 @@
|
||||
Fix `/sync` caching transient errors for the `sync_response_cache_duration`.
|
||||
@@ -0,0 +1 @@
|
||||
Fixed the Admin API user endpoint documentation examples to use JSON booleans (true/false) instead of numeric (0/1) values.
|
||||
@@ -0,0 +1 @@
|
||||
Force keyword-args for clear `default_config(server_name="test")` usage in test utilities.
|
||||
@@ -0,0 +1 @@
|
||||
Fix local events being deleted by the [Purge History admin API](https://element-hq.github.io/synapse/v1.155/admin_api/purge_history_api.html) despite `delete_local_events` being set to false, in room versions other than 1 and 2.
|
||||
@@ -0,0 +1,3 @@
|
||||
The `GET /_matrix/client/unstable/org.matrix.msc1763/retention/configuration` endpoint is now provided when retention
|
||||
is enabled and `experimental_features.msc1763_enabled` is enabled, based on
|
||||
[MSC1763](https://github.com/matrix-org/matrix-spec-proposals/pull/1763).
|
||||
@@ -0,0 +1 @@
|
||||
Add `.ruff_cache/` directory to `.gitignore`.
|
||||
@@ -0,0 +1 @@
|
||||
Provide remote servers a way to find out about an event created during the remote join handshake. Contributed by @FrenchGithubUser and @jason-famedly @ Famedly.
|
||||
@@ -0,0 +1 @@
|
||||
Provide remote servers a way to find out about an event created during the remote join handshake. Contributed by @FrenchGithubUser and @jason-famedly @ Famedly.
|
||||
@@ -0,0 +1 @@
|
||||
Bump `poetry` in CI from `2.2.1` to `2.4.1`.
|
||||
@@ -0,0 +1 @@
|
||||
Split out `deferred` and `tokio_runtime` to their own Rust modules.
|
||||
@@ -0,0 +1 @@
|
||||
Make simple_select_one_onecol_txn() more helpful by naming the table of the select - as all other query wrapper functions already did.
|
||||
@@ -0,0 +1 @@
|
||||
Bump `poetry` in CI from `2.2.1` to `2.4.1`.
|
||||
@@ -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/gorilla/mux v1.8.0 // 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
|
||||
|
||||
@@ -38,6 +38,8 @@ 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/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/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI=
|
||||
github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So=
|
||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.28.0 h1:HWRh5R2+9EifMyIHV7ZV+MIZqgz+PMpZ14Jynv3O2Zs=
|
||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.28.0/go.mod h1:JfhWUomR1baixubs02l85lZYYOm7LV6om4ceouMv45c=
|
||||
github.com/h2non/parth v0.0.0-20190131123155-b4df798d6542 h1:2VTzZjLZBgl62/EtslCrtky5vbi9dd7HrQPQIx6wqiw=
|
||||
|
||||
@@ -0,0 +1,187 @@
|
||||
package synapse_tests
|
||||
|
||||
import (
|
||||
"context"
|
||||
"io"
|
||||
"net/http"
|
||||
"sync/atomic"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/matrix-org/complement"
|
||||
|
||||
"github.com/matrix-org/gomatrixserverlib"
|
||||
|
||||
"github.com/tidwall/gjson"
|
||||
|
||||
"github.com/matrix-org/complement/b"
|
||||
"github.com/matrix-org/complement/federation"
|
||||
"github.com/matrix-org/complement/helpers"
|
||||
"github.com/matrix-org/complement/must"
|
||||
)
|
||||
|
||||
// This test verifies that events sent into a room between a /make_join and
|
||||
// /send_join are not lost to the joining server. When an event is created
|
||||
// during the join handshake, the join event's prev_events (set at make_join
|
||||
// time) won't reference it, creating two forward extremities. The server
|
||||
// handling the join should ensure the joining server can discover the missed
|
||||
// event, for example by sending a follow-up event that references both
|
||||
// extremities, prompting the joining server to backfill.
|
||||
//
|
||||
// See https://github.com/element-hq/synapse/pull/19390
|
||||
//
|
||||
// This test lives as a in-repo Synapse Complement test because the spec doesn't mandate
|
||||
// which events should be resolvable after the `/make_join`/`/send_join` dance (or that
|
||||
// a homeserver should send `m.dummy` events to tie things together).
|
||||
//
|
||||
// To be clear, resolving the events in the `/make_join`/`/send_join` gap would happen
|
||||
// naturally as soon as someone else sends an event who is on a homeserver aware of the
|
||||
// events in the gap (tie it into the DAG). The goal of the automatic dummy events is to
|
||||
// make this happen more immediately by sending a `m.dummy` event that ties things in
|
||||
// instead of waiting for another event to be sent naturally.
|
||||
func TestEventBetweenMakeJoinAndSendJoinIsNotLost(t *testing.T) {
|
||||
deployment := complement.Deploy(t, 1)
|
||||
defer deployment.Destroy(t)
|
||||
|
||||
alice := deployment.Register(t, "hs1", helpers.RegistrationOpts{})
|
||||
|
||||
// We track the message event ID sent between make_join and send_join.
|
||||
// After send_join, we wait for hs1 to send us either:
|
||||
// - the message event itself, or
|
||||
// - any event whose prev_events reference the message (e.g. a dummy event)
|
||||
//
|
||||
// atomic.Value is used because messageEventID is written on the main goroutine and
|
||||
// read on the HTTP handler goroutine, and needs synchronization (without
|
||||
// synchronization, writes are not guaranteed to be observed by other goroutines)
|
||||
var messageEventID atomic.Value
|
||||
messageEventID.Store("")
|
||||
messageDiscoverableWaiter := helpers.NewWaiter()
|
||||
|
||||
srv := federation.NewServer(t, deployment,
|
||||
// hs1 fetches our signing keys via /_matrix/key/v2/server to verify our
|
||||
// identity before accepting federation requests. Without this handler,
|
||||
// make_join is rejected with 401 M_UNAUTHORIZED.
|
||||
federation.HandleKeyRequests(),
|
||||
)
|
||||
// After send_join, hs1 will start sending us federation transactions via
|
||||
// /_matrix/federation/v1/send/{txnID}. Since we handle /send manually
|
||||
// below, any other requests (e.g. key fetches) that arrive unexpectedly
|
||||
// should be tolerated rather than treated as test failures.
|
||||
srv.UnexpectedRequestsAreErrors = false
|
||||
|
||||
// Custom /send handler: hs1 will push new room events to us via federation
|
||||
// transactions once we've joined. We use a raw handler because the
|
||||
// Complement server is not fully in the room until send_join completes, so
|
||||
// we can't use HandleTransactionRequests (which requires the room in
|
||||
// srv.rooms). Instead we parse the raw transaction body ourselves.
|
||||
srv.Mux().
|
||||
Handle("/_matrix/federation/v1/send/{transactionID}", http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
|
||||
body, err := io.ReadAll(req.Body)
|
||||
must.NotError(t, "failed to read request body in /send handler: %v", err)
|
||||
txn := gjson.ParseBytes(body)
|
||||
txn.Get("pdus").ForEach(func(_, pdu gjson.Result) bool {
|
||||
eventID := pdu.Get("event_id").String()
|
||||
eventType := pdu.Get("type").String()
|
||||
t.Logf("Received PDU via /send: type=%s id=%s", eventType, eventID)
|
||||
|
||||
// messageEventID is set after make_join but before send_join.
|
||||
// Transactions can arrive before that window, so skip PDUs that
|
||||
// arrive before we know which event to look for.
|
||||
msgID, _ := messageEventID.Load().(string)
|
||||
if msgID == "" {
|
||||
return true
|
||||
}
|
||||
|
||||
// Check if this IS the message event (server pushed it directly).
|
||||
if eventID == msgID {
|
||||
messageDiscoverableWaiter.Finish()
|
||||
return true
|
||||
}
|
||||
|
||||
// Check if this event's prev_events directly reference the message (e.g. a dummy
|
||||
// event tying the two forward extremities together). If so, the joining server
|
||||
// can backfill from that event and will discover the message.
|
||||
//
|
||||
// XXX: We only check one level of prev_events: if the reference is deeper in the
|
||||
// DAG, it's valid and the joining server can still reach the message through
|
||||
// backfill but our checks don't account for that yet (feel free to edit this
|
||||
// assertion if you run into this)
|
||||
pdu.Get("prev_events").ForEach(func(_, prevEvent gjson.Result) bool {
|
||||
if prevEvent.String() == msgID {
|
||||
messageDiscoverableWaiter.Finish()
|
||||
return false
|
||||
}
|
||||
return true
|
||||
})
|
||||
|
||||
return true
|
||||
})
|
||||
w.WriteHeader(200)
|
||||
// Respond with an empty PDU error map, which is the federation /send
|
||||
// success response format: each key would be a PDU ID whose processing
|
||||
// failed; an empty object means all PDUs were accepted.
|
||||
_, err = w.Write([]byte(`{"pdus":{}}`))
|
||||
must.NotError(t, "failed to write response body in /send handler: %v", err)
|
||||
})).
|
||||
Methods("PUT")
|
||||
|
||||
cancel := srv.Listen()
|
||||
defer cancel()
|
||||
|
||||
// Alice creates a room on hs1.
|
||||
roomID := alice.MustCreateRoom(t, map[string]interface{}{
|
||||
"preset": "public_chat",
|
||||
})
|
||||
|
||||
charlie := srv.UserID("charlie")
|
||||
origin := srv.ServerName()
|
||||
fedClient := srv.FederationClient(deployment)
|
||||
|
||||
// Step 1: make_join, hs1 returns a join event template whose prev_events
|
||||
// reflect the current room DAG tips.
|
||||
makeJoinResp, err := fedClient.MakeJoin(
|
||||
context.Background(), origin,
|
||||
deployment.GetFullyQualifiedHomeserverName(t, "hs1"),
|
||||
roomID, charlie,
|
||||
)
|
||||
must.NotError(t, "MakeJoin", err)
|
||||
|
||||
// Step 2: Alice sends a message on hs1. This advances the DAG past the
|
||||
// point captured by make_join's prev_events. The Complement server is not
|
||||
// yet in the room, so it won't receive this event via normal federation.
|
||||
messageEventID.Store(alice.SendEventSynced(t, roomID, b.Event{
|
||||
Type: "m.room.message",
|
||||
Content: map[string]interface{}{
|
||||
"msgtype": "m.text",
|
||||
"body": "Message sent between make_join and send_join",
|
||||
},
|
||||
}))
|
||||
t.Logf("Alice sent message %s between make_join and send_join", messageEventID.Load())
|
||||
|
||||
// Step 3: Build and sign the join event, then send_join.
|
||||
// The join event's prev_events are from step 1 (before the message),
|
||||
// so persisting it on hs1 creates two forward extremities: the message
|
||||
// and the join.
|
||||
verImpl, err := gomatrixserverlib.GetRoomVersion(makeJoinResp.RoomVersion)
|
||||
must.NotError(t, "GetRoomVersion", err)
|
||||
eb := verImpl.NewEventBuilderFromProtoEvent(&makeJoinResp.JoinEvent)
|
||||
joinEvent, err := eb.Build(time.Now(), srv.ServerName(), srv.KeyID, srv.Priv)
|
||||
must.NotError(t, "Build join event", err)
|
||||
|
||||
_, err = fedClient.SendJoin(
|
||||
context.Background(), origin,
|
||||
deployment.GetFullyQualifiedHomeserverName(t, "hs1"),
|
||||
joinEvent,
|
||||
)
|
||||
must.NotError(t, "SendJoin", err)
|
||||
|
||||
// Step 4: hs1 should make the missed message discoverable to the joining
|
||||
// server. We accept either receiving the message event directly, or
|
||||
// receiving any event whose prev_events reference it (allowing the
|
||||
// joining server to backfill).
|
||||
messageDiscoverableWaiter.Waitf(t, 5*time.Second,
|
||||
"Timed out waiting for message event %s to become discoverable — "+
|
||||
"the event sent between make_join and send_join was lost to the "+
|
||||
"joining server", messageEventID.Load(),
|
||||
)
|
||||
}
|
||||
Vendored
+1
-1
@@ -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.2.1 poetry-plugin-export==1.9.0
|
||||
pip install poetry==2.4.1 poetry-plugin-export==1.9.0
|
||||
poetry export \
|
||||
--extras all \
|
||||
--extras test \
|
||||
|
||||
Vendored
+12
@@ -1,3 +1,15 @@
|
||||
matrix-synapse-py3 (1.155.0+nmu1) UNRELEASED; urgency=medium
|
||||
|
||||
* Bump poetry from 2.2.1 to 2.4.1.
|
||||
|
||||
-- Synapse Packaging team <work@izzy> Wed, 24 Jun 2026 13:10:00 +0100
|
||||
|
||||
matrix-synapse-py3 (1.155.0) stable; urgency=medium
|
||||
|
||||
* New Synapse release 1.155.0.
|
||||
|
||||
-- Synapse Packaging team <packages@matrix.org> Tue, 16 Jun 2026 14:44:04 +0100
|
||||
|
||||
matrix-synapse-py3 (1.155.0~rc1) stable; urgency=medium
|
||||
|
||||
* New Synapse release 1.155.0rc1.
|
||||
|
||||
+1
-1
@@ -22,7 +22,7 @@
|
||||
|
||||
ARG DEBIAN_VERSION=trixie
|
||||
ARG PYTHON_VERSION=3.13
|
||||
ARG POETRY_VERSION=2.2.1
|
||||
ARG POETRY_VERSION=2.4.1
|
||||
|
||||
###
|
||||
### Stage 0: generate requirements.txt
|
||||
|
||||
@@ -34,11 +34,11 @@ It returns a JSON body like the following:
|
||||
}
|
||||
],
|
||||
"avatar_url": "<avatar_url>", // can be null if not set
|
||||
"is_guest": 0,
|
||||
"admin": 0,
|
||||
"deactivated": 0,
|
||||
"is_guest": false,
|
||||
"admin": false,
|
||||
"deactivated": false,
|
||||
"erased": false,
|
||||
"shadow_banned": 0,
|
||||
"shadow_banned": false,
|
||||
"creation_ts": 1560432506,
|
||||
"last_seen_ts": 1732919539393,
|
||||
"appservice_id": null,
|
||||
@@ -183,24 +183,24 @@ A response body like the following is returned:
|
||||
"users": [
|
||||
{
|
||||
"name": "<user_id1>",
|
||||
"is_guest": 0,
|
||||
"admin": 0,
|
||||
"is_guest": false,
|
||||
"admin": false,
|
||||
"user_type": null,
|
||||
"deactivated": 0,
|
||||
"deactivated": false,
|
||||
"erased": false,
|
||||
"shadow_banned": 0,
|
||||
"shadow_banned": false,
|
||||
"displayname": "<User One>",
|
||||
"avatar_url": null,
|
||||
"creation_ts": 1560432668000,
|
||||
"locked": false
|
||||
}, {
|
||||
"name": "<user_id2>",
|
||||
"is_guest": 0,
|
||||
"admin": 1,
|
||||
"is_guest": false,
|
||||
"admin": true,
|
||||
"user_type": null,
|
||||
"deactivated": 0,
|
||||
"deactivated": false,
|
||||
"erased": false,
|
||||
"shadow_banned": 0,
|
||||
"shadow_banned": false,
|
||||
"displayname": "<User Two>",
|
||||
"avatar_url": "<avatar_url>",
|
||||
"creation_ts": 1561550621000,
|
||||
|
||||
@@ -79,11 +79,17 @@ concerned about the criteria for selecting minimum versions. The only thing of c
|
||||
is making sure we're not making it unnecessarily difficult for downstream package
|
||||
maintainers. Generally, this just means avoiding the bleeding edge for a few months.
|
||||
|
||||
The situation for Rust dependencies is fundamentally different. For packagers, the
|
||||
concerns around Python dependency versions do not apply. The `cargo` tool handles
|
||||
downloading and building all libraries to satisfy dependencies, and these libraries are
|
||||
statically linked into the final binary. This means that from a packager's perspective,
|
||||
the Rust dependency versions are an internal build detail, not a runtime dependency to
|
||||
be managed on the target system. Consequently, we have even greater flexibility to
|
||||
upgrade Rust dependencies as needed for the project. Some distros (e.g. Fedora) do
|
||||
package Rust libraries, but this appears to be the outlier rather than the norm.
|
||||
The situation for Rust dependencies is typically different and
|
||||
the concerns around Python dependency versions typically do not apply.
|
||||
For example, for packagers of Debian, the packagers have the choice of either
|
||||
using a crate packaged in the distro, or vendoring crates in the source package for the
|
||||
application.
|
||||
|
||||
This freedom to vendor a dependency crate for a specific application consequently gives
|
||||
us even greater flexibility to upgrade Rust dependencies as needed for the project.
|
||||
|
||||
(This is in contrast with Python dependencies, which are generally
|
||||
installed system-wide by mainstream distributions' official packages.)
|
||||
|
||||
Some distros (e.g. Fedora) do not vendor Rust dependencies in their
|
||||
official application packages, but these cases appear to be less common.
|
||||
@@ -1971,7 +1971,7 @@ rc_presence:
|
||||
|
||||
*(object)* Ratelimiting settings for delayed event management.
|
||||
|
||||
This is a ratelimiting option that ratelimits attempts to restart, cancel, or view delayed events based on the sending client's account and device ID.
|
||||
This is a ratelimiting option that ratelimits attempts to restart, cancel, or view delayed events based on the sending client's account, or its source IP when requests are unauthenticated.
|
||||
|
||||
Attempts to create or send delayed events are ratelimited not by this setting, but by `rc_message`.
|
||||
|
||||
@@ -2870,6 +2870,8 @@ enable_3pid_changes: false
|
||||
|
||||
By default, any room aliases included in this list will be created as a publicly joinable room when the first user registers for the homeserver. If the room already exists, make certain it is a publicly joinable room, i.e. the join rule of the room must be set to `public`. You can find more options relating to auto-joining rooms below.
|
||||
|
||||
Invite-only rooms can also be auto-joined when setting `auto_join_mxid_localpart` to a user who's part of the invite-only rooms.
|
||||
|
||||
As Spaces are just rooms under the hood, Space aliases may also be used.
|
||||
|
||||
Defaults to `[]`.
|
||||
|
||||
@@ -239,6 +239,7 @@ information.
|
||||
^/_matrix/client/(v1|unstable)/rooms/.*/relations/
|
||||
^/_matrix/client/v1/rooms/.*/threads$
|
||||
^/_matrix/client/unstable/im.nheko.summary/summary/.*$
|
||||
^/_matrix/client/v1/room_summary/.*$
|
||||
^/_matrix/client/(r0|v3|unstable)/account/3pid$
|
||||
^/_matrix/client/(r0|v3|unstable)/account/whoami$
|
||||
^/_matrix/client/(r0|v3|unstable)/account/deactivate$
|
||||
|
||||
Generated
+90
-90
@@ -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 == \"all\" or extra == \"jwt\" or extra == \"oidc\""
|
||||
markers = "extra == \"oidc\" or extra == \"jwt\" or extra == \"all\""
|
||||
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 = "platform_machine != \"ppc64le\" and platform_machine != \"s390x\" and python_version < \"3.12\""
|
||||
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"},
|
||||
@@ -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 == \"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"},
|
||||
@@ -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 == \"all\" or extra == \"saml2\""
|
||||
markers = "extra == \"saml2\" or extra == \"all\""
|
||||
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 == \"all\" or extra == \"redis\""
|
||||
markers = "extra == \"redis\" or extra == \"all\""
|
||||
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"},
|
||||
@@ -889,7 +889,7 @@ description = "Read metadata from Python packages"
|
||||
optional = false
|
||||
python-versions = ">=3.9"
|
||||
groups = ["dev"]
|
||||
markers = "platform_machine != \"ppc64le\" and platform_machine != \"s390x\" and python_version < \"3.12\""
|
||||
markers = "python_version < \"3.12\" and platform_machine != \"ppc64le\" and platform_machine != \"s390x\""
|
||||
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"},
|
||||
@@ -930,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"},
|
||||
]
|
||||
@@ -1122,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"},
|
||||
@@ -1239,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.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"},
|
||||
@@ -1547,7 +1547,7 @@ description = "An LDAP3 auth provider for Synapse"
|
||||
optional = true
|
||||
python-versions = ">=3.10"
|
||||
groups = ["main"]
|
||||
markers = "extra == \"all\" or extra == \"matrix-synapse-ldap3\""
|
||||
markers = "extra == \"matrix-synapse-ldap3\" or extra == \"all\""
|
||||
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"},
|
||||
@@ -1828,7 +1828,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"},
|
||||
]
|
||||
@@ -1883,14 +1883,14 @@ tests = ["pytest (>=9)", "typing-extensions (>=4.15)"]
|
||||
|
||||
[[package]]
|
||||
name = "phonenumbers"
|
||||
version = "9.0.26"
|
||||
version = "9.0.28"
|
||||
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.26-py2.py3-none-any.whl", hash = "sha256:ff473da5712965b6c7f7a31cbff8255864df694eb48243771133ecb761e807c1"},
|
||||
{file = "phonenumbers-9.0.26.tar.gz", hash = "sha256:9e582c827f0f5503cddeebef80099475a52ffa761551d8384099c7ec71298cbf"},
|
||||
{file = "phonenumbers-9.0.28-py2.py3-none-any.whl", hash = "sha256:ee8caabab4fd554efb6119e7b95cb69da40e0f04050611730eed839d93f39920"},
|
||||
{file = "phonenumbers-9.0.28.tar.gz", hash = "sha256:f1d810aaa43fbf3a5cb1ee54733218f8333a2a92c85c4d579a810403d6260a8c"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -2026,7 +2026,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"},
|
||||
@@ -2044,7 +2044,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"},
|
||||
]
|
||||
@@ -2060,7 +2060,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"},
|
||||
]
|
||||
@@ -2342,7 +2342,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"},
|
||||
@@ -2474,7 +2474,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"},
|
||||
@@ -2499,7 +2499,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 == \"all\" or extra == \"saml2\""
|
||||
markers = "extra == \"saml2\" or extra == \"all\""
|
||||
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"},
|
||||
@@ -2510,14 +2510,14 @@ six = ">=1.5"
|
||||
|
||||
[[package]]
|
||||
name = "python-multipart"
|
||||
version = "0.0.26"
|
||||
version = "0.0.27"
|
||||
description = "A streaming multipart parser for Python"
|
||||
optional = false
|
||||
python-versions = ">=3.10"
|
||||
groups = ["main"]
|
||||
files = [
|
||||
{file = "python_multipart-0.0.26-py3-none-any.whl", hash = "sha256:c0b169f8c4484c13b0dcf2ef0ec3a4adb255c4b7d18d8e420477d2b1dd03f185"},
|
||||
{file = "python_multipart-0.0.26.tar.gz", hash = "sha256:08fadc45918cd615e26846437f50c5d6d23304da32c341f289a617127b081f17"},
|
||||
{file = "python_multipart-0.0.27-py3-none-any.whl", hash = "sha256:6fccfad17a27334bd0193681b369f476eda3409f17381a2d65aa7df3f7275645"},
|
||||
{file = "python_multipart-0.0.27.tar.gz", hash = "sha256:9870a6a8c5a20a5bf4f07c017bd1489006ff8836cff097b6933355ee2b49b602"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -2527,7 +2527,7 @@ 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-2026.1.post1-py2.py3-none-any.whl", hash = "sha256:f2fd16142fda348286a75e1a524be810bb05d444e5a081f37f7affc635035f7a"},
|
||||
{file = "pytz-2026.1.post1.tar.gz", hash = "sha256:3378dde6a0c3d26719182142c56e60c7f9af7e968076f31aae569d72a0358ee1"},
|
||||
@@ -2668,14 +2668,14 @@ typing-extensions = {version = ">=4.4.0", markers = "python_version < \"3.13\""}
|
||||
|
||||
[[package]]
|
||||
name = "requests"
|
||||
version = "2.33.0"
|
||||
version = "2.33.1"
|
||||
description = "Python HTTP for Humans."
|
||||
optional = false
|
||||
python-versions = ">=3.10"
|
||||
groups = ["main", "dev"]
|
||||
files = [
|
||||
{file = "requests-2.33.0-py3-none-any.whl", hash = "sha256:3324635456fa185245e24865e810cecec7b4caf933d7eb133dcde67d48cee69b"},
|
||||
{file = "requests-2.33.0.tar.gz", hash = "sha256:c7ebc5e8b0f21837386ad0e1c8fe8b829fa5f544d8df3b2253bff14ef29d7652"},
|
||||
{file = "requests-2.33.1-py3-none-any.whl", hash = "sha256:4e6d1ef462f3626a1f0a0a9c42dd93c63bad33f9f1c1937509b8c5c8718ab56a"},
|
||||
{file = "requests-2.33.1.tar.gz", hash = "sha256:18817f8c57c6263968bc123d237e3b8b08ac046f5456bd1e307ee8f4250d3517"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
@@ -2686,7 +2686,6 @@ urllib3 = ">=1.26,<3"
|
||||
|
||||
[package.extras]
|
||||
socks = ["PySocks (>=1.5.6,!=1.5.7)"]
|
||||
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]]
|
||||
@@ -2932,7 +2931,7 @@ 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.57.0-py2.py3-none-any.whl", hash = "sha256:812c8bf5ff3d2f0e89c82f5ce80ab3a6423e102729c4706af7413fd1eb480585"},
|
||||
{file = "sentry_sdk-2.57.0.tar.gz", hash = "sha256:4be8d1e71c32fb27f79c577a337ac8912137bba4bcbc64a4ec1da4d6d8dc5199"},
|
||||
@@ -3038,14 +3037,14 @@ type = ["importlib_metadata (>=7.0.2) ; python_version < \"3.10\"", "jaraco.deve
|
||||
|
||||
[[package]]
|
||||
name = "setuptools-rust"
|
||||
version = "1.12.0"
|
||||
version = "1.12.1"
|
||||
description = "Setuptools Rust extension plugin"
|
||||
optional = false
|
||||
python-versions = ">=3.9"
|
||||
groups = ["main"]
|
||||
files = [
|
||||
{file = "setuptools_rust-1.12.0-py3-none-any.whl", hash = "sha256:7e7db90547f224a835b45f5ad90c983340828a345554a9a660bdb2de8605dcdd"},
|
||||
{file = "setuptools_rust-1.12.0.tar.gz", hash = "sha256:d94a93f0c97751c17014565f07bdc324bee45d396cd1bba83d8e7af92b945f0c"},
|
||||
{file = "setuptools_rust-1.12.1-py3-none-any.whl", hash = "sha256:b7ebd6a182e7aefa97a072e880530c9b0ec8fcca8617e0bb8ff299c1a064f693"},
|
||||
{file = "setuptools_rust-1.12.1.tar.gz", hash = "sha256:85ae70989d96c9cfeb5ef79cf3bac2d5200bc5564f720a06edceedbdf6664640"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
@@ -3132,7 +3131,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"},
|
||||
@@ -3148,7 +3147,7 @@ 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.22.0.tar.gz", hash = "sha256:42e8276afbd5f54fe1d364858b6877bc5e5a4a5ed69f6a005b94ca4918fe1466"},
|
||||
]
|
||||
@@ -3160,60 +3159,61 @@ twisted = ["twisted"]
|
||||
|
||||
[[package]]
|
||||
name = "tomli"
|
||||
version = "2.4.0"
|
||||
version = "2.4.1"
|
||||
description = "A lil' TOML parser"
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
groups = ["main", "dev"]
|
||||
files = [
|
||||
{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"},
|
||||
{file = "tomli-2.4.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:f8f0fc26ec2cc2b965b7a3b87cd19c5c6b8c5e5f436b984e85f486d652285c30"},
|
||||
{file = "tomli-2.4.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4ab97e64ccda8756376892c53a72bd1f964e519c77236368527f758fbc36a53a"},
|
||||
{file = "tomli-2.4.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:96481a5786729fd470164b47cdb3e0e58062a496f455ee41b4403be77cb5a076"},
|
||||
{file = "tomli-2.4.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5a881ab208c0baf688221f8cecc5401bd291d67e38a1ac884d6736cbcd8247e9"},
|
||||
{file = "tomli-2.4.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:47149d5bd38761ac8be13a84864bf0b7b70bc051806bc3669ab1cbc56216b23c"},
|
||||
{file = "tomli-2.4.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ec9bfaf3ad2df51ace80688143a6a4ebc09a248f6ff781a9945e51937008fcbc"},
|
||||
{file = "tomli-2.4.1-cp311-cp311-win32.whl", hash = "sha256:ff2983983d34813c1aeb0fa89091e76c3a22889ee83ab27c5eeb45100560c049"},
|
||||
{file = "tomli-2.4.1-cp311-cp311-win_amd64.whl", hash = "sha256:5ee18d9ebdb417e384b58fe414e8d6af9f4e7a0ae761519fb50f721de398dd4e"},
|
||||
{file = "tomli-2.4.1-cp311-cp311-win_arm64.whl", hash = "sha256:c2541745709bad0264b7d4705ad453b76ccd191e64aa6f0fc66b69a293a45ece"},
|
||||
{file = "tomli-2.4.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:c742f741d58a28940ce01d58f0ab2ea3ced8b12402f162f4d534dfe18ba1cd6a"},
|
||||
{file = "tomli-2.4.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:7f86fd587c4ed9dd76f318225e7d9b29cfc5a9d43de44e5754db8d1128487085"},
|
||||
{file = "tomli-2.4.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ff18e6a727ee0ab0388507b89d1bc6a22b138d1e2fa56d1ad494586d61d2eae9"},
|
||||
{file = "tomli-2.4.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:136443dbd7e1dee43c68ac2694fde36b2849865fa258d39bf822c10e8068eac5"},
|
||||
{file = "tomli-2.4.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:5e262d41726bc187e69af7825504c933b6794dc3fbd5945e41a79bb14c31f585"},
|
||||
{file = "tomli-2.4.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:5cb41aa38891e073ee49d55fbc7839cfdb2bc0e600add13874d048c94aadddd1"},
|
||||
{file = "tomli-2.4.1-cp312-cp312-win32.whl", hash = "sha256:da25dc3563bff5965356133435b757a795a17b17d01dbc0f42fb32447ddfd917"},
|
||||
{file = "tomli-2.4.1-cp312-cp312-win_amd64.whl", hash = "sha256:52c8ef851d9a240f11a88c003eacb03c31fc1c9c4ec64a99a0f922b93874fda9"},
|
||||
{file = "tomli-2.4.1-cp312-cp312-win_arm64.whl", hash = "sha256:f758f1b9299d059cc3f6546ae2af89670cb1c4d48ea29c3cacc4fe7de3058257"},
|
||||
{file = "tomli-2.4.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:36d2bd2ad5fb9eaddba5226aa02c8ec3fa4f192631e347b3ed28186d43be6b54"},
|
||||
{file = "tomli-2.4.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:eb0dc4e38e6a1fd579e5d50369aa2e10acfc9cace504579b2faabb478e76941a"},
|
||||
{file = "tomli-2.4.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c7f2c7f2b9ca6bdeef8f0fa897f8e05085923eb091721675170254cbc5b02897"},
|
||||
{file = "tomli-2.4.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f3c6818a1a86dd6dca7ddcaaf76947d5ba31aecc28cb1b67009a5877c9a64f3f"},
|
||||
{file = "tomli-2.4.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d312ef37c91508b0ab2cee7da26ec0b3ed2f03ce12bd87a588d771ae15dcf82d"},
|
||||
{file = "tomli-2.4.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:51529d40e3ca50046d7606fa99ce3956a617f9b36380da3b7f0dd3dd28e68cb5"},
|
||||
{file = "tomli-2.4.1-cp313-cp313-win32.whl", hash = "sha256:2190f2e9dd7508d2a90ded5ed369255980a1bcdd58e52f7fe24b8162bf9fedbd"},
|
||||
{file = "tomli-2.4.1-cp313-cp313-win_amd64.whl", hash = "sha256:8d65a2fbf9d2f8352685bc1364177ee3923d6baf5e7f43ea4959d7d8bc326a36"},
|
||||
{file = "tomli-2.4.1-cp313-cp313-win_arm64.whl", hash = "sha256:4b605484e43cdc43f0954ddae319fb75f04cc10dd80d830540060ee7cd0243cd"},
|
||||
{file = "tomli-2.4.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:fd0409a3653af6c147209d267a0e4243f0ae46b011aa978b1080359fddc9b6cf"},
|
||||
{file = "tomli-2.4.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:a120733b01c45e9a0c34aeef92bf0cf1d56cfe81ed9d47d562f9ed591a9828ac"},
|
||||
{file = "tomli-2.4.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:559db847dc486944896521f68d8190be1c9e719fced785720d2216fe7022b662"},
|
||||
{file = "tomli-2.4.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:01f520d4f53ef97964a240a035ec2a869fe1a37dde002b57ebc4417a27ccd853"},
|
||||
{file = "tomli-2.4.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7f94b27a62cfad8496c8d2513e1a222dd446f095fca8987fceef261225538a15"},
|
||||
{file = "tomli-2.4.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:ede3e6487c5ef5d28634ba3f31f989030ad6af71edfb0055cbbd14189ff240ba"},
|
||||
{file = "tomli-2.4.1-cp314-cp314-win32.whl", hash = "sha256:3d48a93ee1c9b79c04bb38772ee1b64dcf18ff43085896ea460ca8dec96f35f6"},
|
||||
{file = "tomli-2.4.1-cp314-cp314-win_amd64.whl", hash = "sha256:88dceee75c2c63af144e456745e10101eb67361050196b0b6af5d717254dddf7"},
|
||||
{file = "tomli-2.4.1-cp314-cp314-win_arm64.whl", hash = "sha256:b8c198f8c1805dc42708689ed6864951fd2494f924149d3e4bce7710f8eb5232"},
|
||||
{file = "tomli-2.4.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:d4d8fe59808a54658fcc0160ecfb1b30f9089906c50b23bcb4c69eddc19ec2b4"},
|
||||
{file = "tomli-2.4.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7008df2e7655c495dd12d2a4ad038ff878d4ca4b81fccaf82b714e07eae4402c"},
|
||||
{file = "tomli-2.4.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1d8591993e228b0c930c4bb0db464bdad97b3289fb981255d6c9a41aedc84b2d"},
|
||||
{file = "tomli-2.4.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:734e20b57ba95624ecf1841e72b53f6e186355e216e5412de414e3c51e5e3c41"},
|
||||
{file = "tomli-2.4.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:8a650c2dbafa08d42e51ba0b62740dae4ecb9338eefa093aa5c78ceb546fcd5c"},
|
||||
{file = "tomli-2.4.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:504aa796fe0569bb43171066009ead363de03675276d2d121ac1a4572397870f"},
|
||||
{file = "tomli-2.4.1-cp314-cp314t-win32.whl", hash = "sha256:b1d22e6e9387bf4739fbe23bfa80e93f6b0373a7f1b96c6227c32bef95a4d7a8"},
|
||||
{file = "tomli-2.4.1-cp314-cp314t-win_amd64.whl", hash = "sha256:2c1c351919aca02858f740c6d33adea0c5deea37f9ecca1cc1ef9e884a619d26"},
|
||||
{file = "tomli-2.4.1-cp314-cp314t-win_arm64.whl", hash = "sha256:eab21f45c7f66c13f2a9e0e1535309cee140182a9cdae1e041d02e47291e8396"},
|
||||
{file = "tomli-2.4.1-py3-none-any.whl", hash = "sha256:0d85819802132122da43cb86656f8d1f8c6587d54ae7dcaf30e90533028b49fe"},
|
||||
{file = "tomli-2.4.1.tar.gz", hash = "sha256:7c7e1a961a0b2f2472c1ac5b69affa0ae1132c39adcb67aba98568702b9cc23f"},
|
||||
]
|
||||
markers = {main = "python_version < \"3.14\""}
|
||||
|
||||
[[package]]
|
||||
name = "tornado"
|
||||
@@ -3222,7 +3222,7 @@ description = "Tornado is a Python web framework and asynchronous networking lib
|
||||
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.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"},
|
||||
@@ -3354,7 +3354,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"},
|
||||
@@ -3502,14 +3502,14 @@ files = [
|
||||
|
||||
[[package]]
|
||||
name = "types-requests"
|
||||
version = "2.32.4.20260107"
|
||||
version = "2.33.0.20260408"
|
||||
description = "Typing stubs for requests"
|
||||
optional = false
|
||||
python-versions = ">=3.9"
|
||||
python-versions = ">=3.10"
|
||||
groups = ["dev"]
|
||||
files = [
|
||||
{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"},
|
||||
{file = "types_requests-2.33.0.20260408-py3-none-any.whl", hash = "sha256:81f31d5ea4acb39f03be7bc8bed569ba6d5a9c5d97e89f45ac43d819b68ca50f"},
|
||||
{file = "types_requests-2.33.0.20260408.tar.gz", hash = "sha256:95b9a86376807a216b2fb412b47617b202091c3ea7c078f47cc358d5528ccb7b"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
@@ -3615,7 +3615,7 @@ 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.5.1-py3-none-any.whl", hash = "sha256:ec2b2a15c8896c1fcd14dcee34ca30032b99456c3c43ce793fdb9dca2fb4b869"},
|
||||
{file = "xmlschema-2.5.1.tar.gz", hash = "sha256:4f7497de6c8b6dc2c28ad7b9ed6e21d186f4afe248a5bea4f54eedab4da44083"},
|
||||
@@ -3636,7 +3636,7 @@ description = "Backport of pathlib-compatible object wrapper for zip files"
|
||||
optional = false
|
||||
python-versions = ">=3.9"
|
||||
groups = ["dev"]
|
||||
markers = "platform_machine != \"ppc64le\" and platform_machine != \"s390x\" and python_version < \"3.12\""
|
||||
markers = "python_version < \"3.12\" and platform_machine != \"ppc64le\" and platform_machine != \"s390x\""
|
||||
files = [
|
||||
{file = "zipp-3.23.0-py3-none-any.whl", hash = "sha256:071652d6115ed432f5ce1d34c336c0adfd6a884660d1e9712a256d3d3bd4b14e"},
|
||||
{file = "zipp-3.23.0.tar.gz", hash = "sha256:a07157588a12518c9d4034df3fbbee09c814741a33ff63c05fa29d26a2404166"},
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
[project]
|
||||
name = "matrix-synapse"
|
||||
version = "1.155.0rc1"
|
||||
version = "1.155.0"
|
||||
description = "Homeserver for the Matrix decentralised comms protocol"
|
||||
readme = "README.rst"
|
||||
authors = [
|
||||
|
||||
@@ -0,0 +1,138 @@
|
||||
/*
|
||||
* 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:
|
||||
* <https://www.gnu.org/licenses/agpl-3.0.html>.
|
||||
*
|
||||
*/
|
||||
|
||||
use std::{future::Future, sync::OnceLock};
|
||||
|
||||
use once_cell::sync::OnceCell;
|
||||
use pyo3::{create_exception, exceptions::PyException, prelude::*};
|
||||
|
||||
use crate::tokio_runtime::runtime;
|
||||
|
||||
create_exception!(
|
||||
synapse.synapse_rust.http_client,
|
||||
RustPanicError,
|
||||
PyException,
|
||||
"A panic which happened in a Rust future"
|
||||
);
|
||||
|
||||
impl RustPanicError {
|
||||
fn from_panic(panic_err: &(dyn std::any::Any + Send + 'static)) -> PyErr {
|
||||
// Apparently this is how you extract the panic message from a panic
|
||||
let panic_message = if let Some(str_slice) = panic_err.downcast_ref::<&str>() {
|
||||
str_slice
|
||||
} else if let Some(string) = panic_err.downcast_ref::<String>() {
|
||||
string
|
||||
} else {
|
||||
"unknown error"
|
||||
};
|
||||
Self::new_err(panic_message.to_owned())
|
||||
}
|
||||
}
|
||||
|
||||
/// A reference to the `twisted.internet.defer` module.
|
||||
static DEFER: OnceCell<Py<PyAny>> = OnceCell::new();
|
||||
|
||||
/// Access to the `twisted.internet.defer` module.
|
||||
fn defer(py: Python<'_>) -> PyResult<&Bound<'_, PyAny>> {
|
||||
Ok(DEFER
|
||||
.get_or_try_init(|| py.import("twisted.internet.defer").map(Into::into))?
|
||||
.bind(py))
|
||||
}
|
||||
|
||||
/// Creates a twisted deferred from the given future, spawning the task on the
|
||||
/// tokio runtime.
|
||||
///
|
||||
/// Does not handle deferred cancellation or contextvars.
|
||||
pub fn create_deferred<'py, F, O>(
|
||||
py: Python<'py>,
|
||||
reactor: &Bound<'py, PyAny>,
|
||||
fut: F,
|
||||
) -> PyResult<Bound<'py, PyAny>>
|
||||
where
|
||||
F: Future<Output = PyResult<O>> + Send + 'static,
|
||||
for<'a> O: IntoPyObject<'a> + Send + 'static,
|
||||
{
|
||||
let deferred = defer(py)?.call_method0("Deferred")?;
|
||||
let deferred_callback = deferred.getattr("callback")?.unbind();
|
||||
let deferred_errback = deferred.getattr("errback")?.unbind();
|
||||
|
||||
let rt = runtime(reactor)?;
|
||||
let handle = rt.handle()?;
|
||||
let task = handle.spawn(fut);
|
||||
|
||||
// Unbind the reactor so that we can pass it to the task
|
||||
let reactor = reactor.clone().unbind();
|
||||
handle.spawn(async move {
|
||||
let res = task.await;
|
||||
|
||||
Python::attach(move |py| {
|
||||
// Flatten the panic into standard python error
|
||||
let res = match res {
|
||||
Ok(r) => r,
|
||||
Err(join_err) => match join_err.try_into_panic() {
|
||||
Ok(panic_err) => Err(RustPanicError::from_panic(&panic_err)),
|
||||
Err(err) => Err(PyException::new_err(format!("Task cancelled: {err}"))),
|
||||
},
|
||||
};
|
||||
|
||||
// Re-bind the reactor
|
||||
let reactor = reactor.bind(py);
|
||||
|
||||
// Send the result to the deferred, via `.callback(..)` or `.errback(..)`
|
||||
match res {
|
||||
Ok(obj) => {
|
||||
reactor
|
||||
.call_method("callFromThread", (deferred_callback, obj), None)
|
||||
.expect("callFromThread should not fail"); // There's nothing we can really do with errors here
|
||||
}
|
||||
Err(err) => {
|
||||
reactor
|
||||
.call_method("callFromThread", (deferred_errback, err), None)
|
||||
.expect("callFromThread should not fail"); // There's nothing we can really do with errors here
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Make the deferred follow the Synapse logcontext rules
|
||||
make_deferred_yieldable(py, &deferred)
|
||||
}
|
||||
|
||||
static MAKE_DEFERRED_YIELDABLE: OnceLock<pyo3::Py<pyo3::PyAny>> = OnceLock::new();
|
||||
|
||||
/// Given a deferred, make it follow the Synapse logcontext rules
|
||||
fn make_deferred_yieldable<'py>(
|
||||
py: Python<'py>,
|
||||
deferred: &Bound<'py, PyAny>,
|
||||
) -> PyResult<Bound<'py, PyAny>> {
|
||||
let make_deferred_yieldable = MAKE_DEFERRED_YIELDABLE.get_or_init(|| {
|
||||
let sys = PyModule::import(py, "synapse.logging.context").unwrap();
|
||||
let func = sys.getattr("make_deferred_yieldable").unwrap().unbind();
|
||||
func
|
||||
});
|
||||
|
||||
make_deferred_yieldable
|
||||
.call1(py, (deferred,))?
|
||||
.extract(py)
|
||||
.map_err(Into::into)
|
||||
}
|
||||
|
||||
/// Called when registering modules with python.
|
||||
pub fn register_module(py: Python<'_>, _m: &Bound<'_, PyModule>) -> PyResult<()> {
|
||||
// Make sure we fail early if we can't load some modules
|
||||
defer(py)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
+4
-205
@@ -12,145 +12,22 @@
|
||||
* <https://www.gnu.org/licenses/agpl-3.0.html>.
|
||||
*/
|
||||
|
||||
use std::{collections::HashMap, future::Future, sync::OnceLock};
|
||||
use std::collections::HashMap;
|
||||
|
||||
use anyhow::Context;
|
||||
use http_body_util::BodyExt;
|
||||
use once_cell::sync::OnceCell;
|
||||
use pyo3::{create_exception, exceptions::PyException, prelude::*};
|
||||
use pyo3::prelude::*;
|
||||
use reqwest::RequestBuilder;
|
||||
use tokio::runtime::Runtime;
|
||||
|
||||
use crate::deferred::create_deferred;
|
||||
use crate::errors::HttpResponseException;
|
||||
|
||||
create_exception!(
|
||||
synapse.synapse_rust.http_client,
|
||||
RustPanicError,
|
||||
PyException,
|
||||
"A panic which happened in a Rust future"
|
||||
);
|
||||
|
||||
impl RustPanicError {
|
||||
fn from_panic(panic_err: &(dyn std::any::Any + Send + 'static)) -> PyErr {
|
||||
// Apparently this is how you extract the panic message from a panic
|
||||
let panic_message = if let Some(str_slice) = panic_err.downcast_ref::<&str>() {
|
||||
str_slice
|
||||
} else if let Some(string) = panic_err.downcast_ref::<String>() {
|
||||
string
|
||||
} else {
|
||||
"unknown error"
|
||||
};
|
||||
Self::new_err(panic_message.to_owned())
|
||||
}
|
||||
}
|
||||
|
||||
/// This is the name of the attribute where we store the runtime on the reactor
|
||||
static TOKIO_RUNTIME_ATTR: &str = "__synapse_rust_tokio_runtime";
|
||||
|
||||
/// A Python wrapper around a Tokio runtime.
|
||||
///
|
||||
/// This allows us to 'store' the runtime on the reactor instance, starting it
|
||||
/// when the reactor starts, and stopping it when the reactor shuts down.
|
||||
#[pyclass]
|
||||
struct PyTokioRuntime {
|
||||
runtime: Option<Runtime>,
|
||||
}
|
||||
|
||||
#[pymethods]
|
||||
impl PyTokioRuntime {
|
||||
fn start(&mut self) -> PyResult<()> {
|
||||
// TODO: allow customization of the runtime like the number of threads
|
||||
let runtime = tokio::runtime::Builder::new_multi_thread()
|
||||
.worker_threads(4)
|
||||
.enable_all()
|
||||
.build()?;
|
||||
|
||||
self.runtime = Some(runtime);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn shutdown(&mut self) -> PyResult<()> {
|
||||
let runtime = self
|
||||
.runtime
|
||||
.take()
|
||||
.context("Runtime was already shutdown")?;
|
||||
|
||||
// Dropping the runtime will shut it down
|
||||
drop(runtime);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl PyTokioRuntime {
|
||||
/// Get the handle to the Tokio runtime, if it is running.
|
||||
fn handle(&self) -> PyResult<&tokio::runtime::Handle> {
|
||||
let handle = self
|
||||
.runtime
|
||||
.as_ref()
|
||||
.context("Tokio runtime is not running")?
|
||||
.handle();
|
||||
|
||||
Ok(handle)
|
||||
}
|
||||
}
|
||||
|
||||
/// Get a handle to the Tokio runtime stored on the reactor instance, or create
|
||||
/// a new one.
|
||||
fn runtime<'a>(reactor: &Bound<'a, PyAny>) -> PyResult<PyRef<'a, PyTokioRuntime>> {
|
||||
if !reactor.hasattr(TOKIO_RUNTIME_ATTR)? {
|
||||
install_runtime(reactor)?;
|
||||
}
|
||||
|
||||
get_runtime(reactor)
|
||||
}
|
||||
|
||||
/// Install a new Tokio runtime on the reactor instance.
|
||||
fn install_runtime(reactor: &Bound<PyAny>) -> PyResult<()> {
|
||||
let py = reactor.py();
|
||||
let runtime = PyTokioRuntime { runtime: None };
|
||||
let runtime = runtime.into_pyobject(py)?;
|
||||
|
||||
// Attach the runtime to the reactor, starting it when the reactor is
|
||||
// running, stopping it when the reactor is shutting down
|
||||
reactor.call_method1("callWhenRunning", (runtime.getattr("start")?,))?;
|
||||
reactor.call_method1(
|
||||
"addSystemEventTrigger",
|
||||
("after", "shutdown", runtime.getattr("shutdown")?),
|
||||
)?;
|
||||
reactor.setattr(TOKIO_RUNTIME_ATTR, runtime)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Get a reference to a Tokio runtime handle stored on the reactor instance.
|
||||
fn get_runtime<'a>(reactor: &Bound<'a, PyAny>) -> PyResult<PyRef<'a, PyTokioRuntime>> {
|
||||
// This will raise if `TOKIO_RUNTIME_ATTR` is not set or if it is
|
||||
// not a `Runtime`. Careful that this could happen if the user sets it
|
||||
// manually, or if multiple versions of `pyo3-twisted` are used!
|
||||
let runtime: Bound<PyTokioRuntime> = reactor.getattr(TOKIO_RUNTIME_ATTR)?.extract()?;
|
||||
Ok(runtime.borrow())
|
||||
}
|
||||
|
||||
/// A reference to the `twisted.internet.defer` module.
|
||||
static DEFER: OnceCell<Py<PyAny>> = OnceCell::new();
|
||||
|
||||
/// Access to the `twisted.internet.defer` module.
|
||||
fn defer(py: Python<'_>) -> PyResult<&Bound<'_, PyAny>> {
|
||||
Ok(DEFER
|
||||
.get_or_try_init(|| py.import("twisted.internet.defer").map(Into::into))?
|
||||
.bind(py))
|
||||
}
|
||||
use crate::tokio_runtime::runtime;
|
||||
|
||||
/// Called when registering modules with python.
|
||||
pub fn register_module(py: Python<'_>, m: &Bound<'_, PyModule>) -> PyResult<()> {
|
||||
let child_module: Bound<'_, PyModule> = PyModule::new(py, "http_client")?;
|
||||
child_module.add_class::<HttpClient>()?;
|
||||
|
||||
// Make sure we fail early if we can't load some modules
|
||||
defer(py)?;
|
||||
|
||||
m.add_submodule(&child_module)?;
|
||||
|
||||
// We need to manually add the module to sys.modules to make `from
|
||||
@@ -262,81 +139,3 @@ impl HttpClient {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// Creates a twisted deferred from the given future, spawning the task on the
|
||||
/// tokio runtime.
|
||||
///
|
||||
/// Does not handle deferred cancellation or contextvars.
|
||||
fn create_deferred<'py, F, O>(
|
||||
py: Python<'py>,
|
||||
reactor: &Bound<'py, PyAny>,
|
||||
fut: F,
|
||||
) -> PyResult<Bound<'py, PyAny>>
|
||||
where
|
||||
F: Future<Output = PyResult<O>> + Send + 'static,
|
||||
for<'a> O: IntoPyObject<'a> + Send + 'static,
|
||||
{
|
||||
let deferred = defer(py)?.call_method0("Deferred")?;
|
||||
let deferred_callback = deferred.getattr("callback")?.unbind();
|
||||
let deferred_errback = deferred.getattr("errback")?.unbind();
|
||||
|
||||
let rt = runtime(reactor)?;
|
||||
let handle = rt.handle()?;
|
||||
let task = handle.spawn(fut);
|
||||
|
||||
// Unbind the reactor so that we can pass it to the task
|
||||
let reactor = reactor.clone().unbind();
|
||||
handle.spawn(async move {
|
||||
let res = task.await;
|
||||
|
||||
Python::attach(move |py| {
|
||||
// Flatten the panic into standard python error
|
||||
let res = match res {
|
||||
Ok(r) => r,
|
||||
Err(join_err) => match join_err.try_into_panic() {
|
||||
Ok(panic_err) => Err(RustPanicError::from_panic(&panic_err)),
|
||||
Err(err) => Err(PyException::new_err(format!("Task cancelled: {err}"))),
|
||||
},
|
||||
};
|
||||
|
||||
// Re-bind the reactor
|
||||
let reactor = reactor.bind(py);
|
||||
|
||||
// Send the result to the deferred, via `.callback(..)` or `.errback(..)`
|
||||
match res {
|
||||
Ok(obj) => {
|
||||
reactor
|
||||
.call_method("callFromThread", (deferred_callback, obj), None)
|
||||
.expect("callFromThread should not fail"); // There's nothing we can really do with errors here
|
||||
}
|
||||
Err(err) => {
|
||||
reactor
|
||||
.call_method("callFromThread", (deferred_errback, err), None)
|
||||
.expect("callFromThread should not fail"); // There's nothing we can really do with errors here
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Make the deferred follow the Synapse logcontext rules
|
||||
make_deferred_yieldable(py, &deferred)
|
||||
}
|
||||
|
||||
static MAKE_DEFERRED_YIELDABLE: OnceLock<pyo3::Py<pyo3::PyAny>> = OnceLock::new();
|
||||
|
||||
/// Given a deferred, make it follow the Synapse logcontext rules
|
||||
fn make_deferred_yieldable<'py>(
|
||||
py: Python<'py>,
|
||||
deferred: &Bound<'py, PyAny>,
|
||||
) -> PyResult<Bound<'py, PyAny>> {
|
||||
let make_deferred_yieldable = MAKE_DEFERRED_YIELDABLE.get_or_init(|| {
|
||||
let sys = PyModule::import(py, "synapse.logging.context").unwrap();
|
||||
let func = sys.getattr("make_deferred_yieldable").unwrap().unbind();
|
||||
func
|
||||
});
|
||||
|
||||
make_deferred_yieldable
|
||||
.call1(py, (deferred,))?
|
||||
.extract(py)
|
||||
.map_err(Into::into)
|
||||
}
|
||||
@@ -6,6 +6,7 @@ use pyo3_log::ResetHandle;
|
||||
|
||||
pub mod acl;
|
||||
pub mod canonical_json;
|
||||
pub mod deferred;
|
||||
pub mod duration;
|
||||
pub mod errors;
|
||||
pub mod events;
|
||||
@@ -19,6 +20,7 @@ pub mod push;
|
||||
pub mod rendezvous;
|
||||
pub mod room_versions;
|
||||
pub mod segmenter;
|
||||
pub mod tokio_runtime;
|
||||
pub mod types;
|
||||
|
||||
lazy_static! {
|
||||
@@ -65,6 +67,7 @@ fn synapse_rust(py: Python<'_>, m: &Bound<'_, PyModule>) -> PyResult<()> {
|
||||
m.add_function(wrap_pyfunction!(reset_logging_config, m)?)?;
|
||||
|
||||
acl::register_module(py, m)?;
|
||||
deferred::register_module(py, m)?;
|
||||
push::register_module(py, m)?;
|
||||
events::register_module(py, m)?;
|
||||
http_client::register_module(py, m)?;
|
||||
|
||||
@@ -0,0 +1,107 @@
|
||||
/*
|
||||
* 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:
|
||||
* <https://www.gnu.org/licenses/agpl-3.0.html>.
|
||||
*
|
||||
*/
|
||||
|
||||
use anyhow::Context;
|
||||
use pyo3::prelude::*;
|
||||
use tokio::runtime::Runtime;
|
||||
|
||||
/// This is the name of the attribute where we store the runtime on the reactor
|
||||
static TOKIO_RUNTIME_ATTR: &str = "__synapse_rust_tokio_runtime";
|
||||
|
||||
/// A Python wrapper around a Tokio runtime.
|
||||
///
|
||||
/// This allows us to 'store' the runtime on the reactor instance, starting it
|
||||
/// when the reactor starts, and stopping it when the reactor shuts down.
|
||||
#[pyclass]
|
||||
pub struct PyTokioRuntime {
|
||||
runtime: Option<Runtime>,
|
||||
}
|
||||
|
||||
#[pymethods]
|
||||
impl PyTokioRuntime {
|
||||
fn start(&mut self) -> PyResult<()> {
|
||||
// TODO: allow customization of the runtime like the number of threads
|
||||
let runtime = tokio::runtime::Builder::new_multi_thread()
|
||||
.worker_threads(4)
|
||||
.enable_all()
|
||||
.build()?;
|
||||
|
||||
self.runtime = Some(runtime);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn shutdown(&mut self) -> PyResult<()> {
|
||||
let runtime = self
|
||||
.runtime
|
||||
.take()
|
||||
.context("Runtime was already shutdown")?;
|
||||
|
||||
// Dropping the runtime will shut it down
|
||||
drop(runtime);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl PyTokioRuntime {
|
||||
/// Get the handle to the Tokio runtime, if it is running.
|
||||
pub fn handle(&self) -> PyResult<&tokio::runtime::Handle> {
|
||||
let handle = self
|
||||
.runtime
|
||||
.as_ref()
|
||||
.context("Tokio runtime is not running")?
|
||||
.handle();
|
||||
|
||||
Ok(handle)
|
||||
}
|
||||
}
|
||||
|
||||
/// Get a handle to the Tokio runtime stored on the reactor instance, or create
|
||||
/// a new one.
|
||||
pub fn runtime<'a>(reactor: &Bound<'a, PyAny>) -> PyResult<PyRef<'a, PyTokioRuntime>> {
|
||||
if !reactor.hasattr(TOKIO_RUNTIME_ATTR)? {
|
||||
install_runtime(reactor)?;
|
||||
}
|
||||
|
||||
get_runtime(reactor)
|
||||
}
|
||||
|
||||
/// Install a new Tokio runtime on the reactor instance.
|
||||
fn install_runtime(reactor: &Bound<PyAny>) -> PyResult<()> {
|
||||
let py = reactor.py();
|
||||
let runtime = PyTokioRuntime { runtime: None };
|
||||
let runtime = runtime.into_pyobject(py)?;
|
||||
|
||||
// Attach the runtime to the reactor, starting it when the reactor is
|
||||
// running, stopping it when the reactor is shutting down
|
||||
reactor.call_method1("callWhenRunning", (runtime.getattr("start")?,))?;
|
||||
reactor.call_method1(
|
||||
"addSystemEventTrigger",
|
||||
("after", "shutdown", runtime.getattr("shutdown")?),
|
||||
)?;
|
||||
reactor.setattr(TOKIO_RUNTIME_ATTR, runtime)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Get a reference to a Tokio runtime handle stored on the reactor instance.
|
||||
fn get_runtime<'a>(reactor: &Bound<'a, PyAny>) -> PyResult<PyRef<'a, PyTokioRuntime>> {
|
||||
// This will raise if `TOKIO_RUNTIME_ATTR` is not set or if it is
|
||||
// not a `Runtime`. Careful that this could happen if the user sets it
|
||||
// manually, or if multiple versions of `pyo3-twisted` are used!
|
||||
let runtime: Bound<PyTokioRuntime> = reactor.getattr(TOKIO_RUNTIME_ATTR)?.extract()?;
|
||||
Ok(runtime.borrow())
|
||||
}
|
||||
@@ -2244,8 +2244,8 @@ properties:
|
||||
|
||||
|
||||
This is a ratelimiting option that ratelimits attempts to restart, cancel,
|
||||
or view delayed events based on the sending client's account and device
|
||||
ID.
|
||||
or view delayed events based on the sending client's account,
|
||||
or its source IP when requests are unauthenticated.
|
||||
|
||||
|
||||
Attempts to create or send delayed events are ratelimited not by this
|
||||
@@ -3227,6 +3227,10 @@ properties:
|
||||
options relating to auto-joining rooms below.
|
||||
|
||||
|
||||
Invite-only rooms can also be auto-joined when setting `auto_join_mxid_localpart`
|
||||
to a user who's part of the invite-only rooms.
|
||||
|
||||
|
||||
As Spaces are just rooms under the hood, Space aliases may also be used.
|
||||
items:
|
||||
type: string
|
||||
|
||||
+21
-11
@@ -611,18 +611,28 @@ def _wait_for_actions(gh_token: str | None) -> None:
|
||||
if len(resp["workflow_runs"]) == 0:
|
||||
continue
|
||||
|
||||
if all(
|
||||
workflow["status"] != "in_progress" for workflow in resp["workflow_runs"]
|
||||
):
|
||||
success = all(
|
||||
workflow["status"] == "completed" for workflow in resp["workflow_runs"]
|
||||
)
|
||||
if success:
|
||||
_notify("Workflows successful. You can now continue the release.")
|
||||
else:
|
||||
_notify("Workflows failed.")
|
||||
click.confirm("Continue anyway?", abort=True)
|
||||
# Notify early if any workflow run has already failed.
|
||||
failed_workflows = [
|
||||
workflow
|
||||
for workflow in resp["workflow_runs"]
|
||||
if workflow["status"] == "completed" and workflow["conclusion"] != "success"
|
||||
]
|
||||
if failed_workflows:
|
||||
for workflow in failed_workflows:
|
||||
print(
|
||||
f"Workflow run failed ({workflow['conclusion']}): {workflow['name']}"
|
||||
)
|
||||
print(f" see {workflow['html_url']}")
|
||||
_notify("A workflow run has failed.")
|
||||
click.confirm("Continue anyway?", abort=True)
|
||||
break
|
||||
|
||||
# If every run has completed successfully, we are done.
|
||||
if all(
|
||||
workflow["status"] == "completed" and workflow["conclusion"] == "success"
|
||||
for workflow in resp["workflow_runs"]
|
||||
):
|
||||
_notify("Workflows successful. You can now continue the release.")
|
||||
break
|
||||
|
||||
|
||||
|
||||
@@ -497,7 +497,8 @@ def listen_http(
|
||||
reactor=reactor,
|
||||
)
|
||||
logger.info(
|
||||
"Synapse now listening on TCP port %d (TLS)", listener_config.port
|
||||
"Synapse now listening on TCP port %d (TLS)",
|
||||
ports[0].getHost().port,
|
||||
)
|
||||
else:
|
||||
ports = listen_tcp(
|
||||
@@ -507,7 +508,7 @@ def listen_http(
|
||||
reactor=reactor,
|
||||
)
|
||||
logger.info(
|
||||
"Synapse now listening on TCP port %d", listener_config.port
|
||||
"Synapse now listening on TCP port %d", ports[0].getHost().port
|
||||
)
|
||||
|
||||
elif isinstance(listener_config, UnixListenerConfig):
|
||||
|
||||
@@ -101,6 +101,7 @@ class ApplicationService:
|
||||
rate_limited: bool = True,
|
||||
ip_range_whitelist: IPSet | None = None,
|
||||
supports_ephemeral: bool = False,
|
||||
supports_unstable_ephemeral: bool = False,
|
||||
msc3202_transaction_extensions: bool = False,
|
||||
msc4190_device_management: bool = False,
|
||||
):
|
||||
@@ -125,6 +126,7 @@ class ApplicationService:
|
||||
self.namespaces = self._check_namespaces(namespaces)
|
||||
self.id = id
|
||||
self.ip_range_whitelist = ip_range_whitelist
|
||||
self.supports_unstable_ephemeral = supports_unstable_ephemeral
|
||||
self.supports_ephemeral = supports_ephemeral
|
||||
self.msc3202_transaction_extensions = msc3202_transaction_extensions
|
||||
self.msc4190_device_management = msc4190_device_management
|
||||
|
||||
@@ -357,8 +357,21 @@ class ApplicationServiceApi(SimpleHttpClient):
|
||||
if service.supports_ephemeral:
|
||||
body.update(
|
||||
{
|
||||
# TODO: Update to stable prefixes once MSC2409 completes FCP merge.
|
||||
"ephemeral": ephemeral,
|
||||
}
|
||||
)
|
||||
if service.supports_unstable_ephemeral:
|
||||
body.update(
|
||||
{
|
||||
"de.sorunome.msc2409.ephemeral": ephemeral,
|
||||
}
|
||||
)
|
||||
if service.supports_ephemeral or service.supports_unstable_ephemeral:
|
||||
body.update(
|
||||
{
|
||||
# TODO: Update to stable prefixes once MSC4203 completes FCP merge.
|
||||
# Previously, this was part of MSC2409 which is why it has the
|
||||
# mismatched unstable identifier
|
||||
"de.sorunome.msc2409.to_device": to_device_messages,
|
||||
}
|
||||
)
|
||||
|
||||
@@ -169,7 +169,12 @@ def _load_appservice(
|
||||
if as_info.get("ip_range_whitelist"):
|
||||
ip_range_whitelist = IPSet(as_info.get("ip_range_whitelist"))
|
||||
|
||||
supports_ephemeral = as_info.get("de.sorunome.msc2409.push_ephemeral", False)
|
||||
# TODO: remove push_ephemeral handling at some point in the future. It was part of
|
||||
# MSC2409 which changed the identifier near the end of the review cycle.
|
||||
supports_unstable_ephemeral = as_info.get(
|
||||
"de.sorunome.msc2409.push_ephemeral", False
|
||||
)
|
||||
supports_ephemeral = as_info.get("receive_ephemeral", False)
|
||||
|
||||
# Opt-in flag for the MSC3202-specific transactional behaviour.
|
||||
# When enabled, appservice transactions contain the following information:
|
||||
@@ -204,6 +209,7 @@ def _load_appservice(
|
||||
protocols=protocols,
|
||||
rate_limited=rate_limited,
|
||||
ip_range_whitelist=ip_range_whitelist,
|
||||
supports_unstable_ephemeral=supports_unstable_ephemeral,
|
||||
supports_ephemeral=supports_ephemeral,
|
||||
msc3202_transaction_extensions=msc3202_transaction_extensions,
|
||||
msc4190_device_management=msc4190_enabled,
|
||||
|
||||
@@ -380,6 +380,9 @@ class ExperimentalConfig(Config):
|
||||
) -> None:
|
||||
experimental = config.get("experimental_features") or {}
|
||||
|
||||
# MSC1763 (retention policy configuration endpoint)
|
||||
self.msc1763_enabled: bool = experimental.get("msc1763_enabled", False)
|
||||
|
||||
# MSC3026 (busy presence state)
|
||||
self.msc3026_enabled: bool = experimental.get("msc3026_enabled", False)
|
||||
|
||||
@@ -389,6 +392,8 @@ class ExperimentalConfig(Config):
|
||||
# MSC2409 (this setting only relates to optionally sending to-device messages).
|
||||
# Presence, typing and read receipt EDUs are already sent to application services that
|
||||
# have opted in to receive them. If enabled, this adds to-device messages to that list.
|
||||
# This is also for MSC4203 which was broken off of MSC2409 but kept the same unstable
|
||||
# identifier.
|
||||
self.msc2409_to_device_messages_enabled: bool = experimental.get(
|
||||
"msc2409_to_device_messages_enabled", False
|
||||
)
|
||||
|
||||
@@ -816,6 +816,15 @@ class FederationServer(FederationBase):
|
||||
event, context = await self._on_send_membership_event(
|
||||
origin, content, Membership.JOIN, room_id
|
||||
)
|
||||
# Use the join event's own stream ordering as the upper bound when fetching
|
||||
# forward extremities (below), so we only consider extremities that existed at
|
||||
# or before the join rather than those introduced by concurrent writes that
|
||||
# occur while we prepare the response.
|
||||
# Note: in workers mode the event is persisted on a separate worker, so
|
||||
# event.internal_metadata.stream_ordering is not populated here; query the DB.
|
||||
stream_ordering_of_join = (
|
||||
await self.store.get_position_for_event(event.event_id)
|
||||
).stream
|
||||
|
||||
prev_state_ids = await context.get_prev_state_ids()
|
||||
|
||||
@@ -856,6 +865,30 @@ class FederationServer(FederationBase):
|
||||
"members_omitted": caller_supports_partial_state,
|
||||
}
|
||||
|
||||
# Check the forward extremities for the room here. If there is more than one, it
|
||||
# is likely that another event was created in the room during the
|
||||
# make_join/send_join handshake. The joining server is likely to thus miss this event
|
||||
# until a second event is created that references it - which could be some time.
|
||||
# In that case, we proactively send a dummy extensible event that ties these
|
||||
# forward extremities together. The remote server will then attempt to backfill
|
||||
# the missing event on its own.
|
||||
#
|
||||
# By not sending the 'missing event' directly, but instead having the joining
|
||||
# homeserver backfill it, the stream ordering for the missing event will be
|
||||
# "before" the join (which is what we expect).
|
||||
|
||||
forward_extremities = (
|
||||
await self.store.get_forward_extremities_for_room_at_stream_ordering(
|
||||
room_id, stream_ordering_of_join
|
||||
)
|
||||
)
|
||||
|
||||
if len(forward_extremities) > 1:
|
||||
# The likelihood of this being used is extremely low, thus only build the handler
|
||||
# when necessary.
|
||||
_creation_handler = self.hs.get_event_creation_handler()
|
||||
await _creation_handler._send_dummy_event_after_room_join(room_id)
|
||||
|
||||
if servers_in_room is not None:
|
||||
resp["servers_in_room"] = list(servers_in_room)
|
||||
|
||||
|
||||
@@ -255,9 +255,8 @@ class ApplicationServicesHandler:
|
||||
will cause this function to return early.
|
||||
|
||||
Ephemeral events will only be pushed to appservices that have opted into
|
||||
receiving them by setting `push_ephemeral` to true in their registration
|
||||
file. Note that while MSC2409 is experimental, this option is called
|
||||
`de.sorunome.msc2409.push_ephemeral`.
|
||||
receiving them by setting `receive_ephemeral` to true in their
|
||||
registration file(previously this was `push_ephemeral`).
|
||||
|
||||
Appservices will only receive ephemeral events that fall within their
|
||||
registered user and room namespaces.
|
||||
@@ -270,7 +269,6 @@ class ApplicationServicesHandler:
|
||||
|
||||
# Notify appservices of updates in ephemeral event streams.
|
||||
# Only the following streams are currently supported.
|
||||
# FIXME: We should use constants for these values.
|
||||
if stream_key not in (
|
||||
StreamKeyType.TYPING,
|
||||
StreamKeyType.RECEIPT,
|
||||
|
||||
@@ -36,6 +36,7 @@ from synapse.storage.databases.main.delayed_events import (
|
||||
)
|
||||
from synapse.storage.databases.main.state_deltas import StateDelta
|
||||
from synapse.types import (
|
||||
Absent,
|
||||
JsonDict,
|
||||
Requester,
|
||||
RoomID,
|
||||
@@ -45,7 +46,6 @@ from synapse.types import (
|
||||
from synapse.util.duration import Duration
|
||||
from synapse.util.events import generate_fake_event_id
|
||||
from synapse.util.metrics import Measure
|
||||
from synapse.util.sentinel import Sentinel
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from synapse.server import HomeServer
|
||||
@@ -61,13 +61,13 @@ class DelayedEventsHandler:
|
||||
self._storage_controllers = hs.get_storage_controllers()
|
||||
self._config = hs.config
|
||||
self._clock = hs.get_clock()
|
||||
self._auth = hs.get_auth()
|
||||
self._event_creation_handler = hs.get_event_creation_handler()
|
||||
self._room_member_handler = hs.get_room_member_handler()
|
||||
|
||||
self._request_ratelimiter = hs.get_request_ratelimiter()
|
||||
|
||||
# Ratelimiter for management of existing delayed events,
|
||||
# keyed by the sending user ID & device ID.
|
||||
# Ratelimiter for management of existing delayed events
|
||||
self._delayed_event_mgmt_ratelimiter = Ratelimiter(
|
||||
store=self._store,
|
||||
clock=self._clock,
|
||||
@@ -273,9 +273,7 @@ class DelayedEventsHandler:
|
||||
)
|
||||
continue
|
||||
|
||||
sender_str = event_id_and_sender_dict.get(
|
||||
delta.event_id, Sentinel.UNSET_SENTINEL
|
||||
)
|
||||
sender_str = event_id_and_sender_dict.get(delta.event_id, Absent)
|
||||
if sender_str is None:
|
||||
# An event exists, but the `sender` field was "null" and Synapse
|
||||
# incorrectly accepted the event. This is not expected.
|
||||
@@ -285,7 +283,7 @@ class DelayedEventsHandler:
|
||||
delta.event_id,
|
||||
)
|
||||
continue
|
||||
if sender_str is Sentinel.UNSET_SENTINEL:
|
||||
if sender_str is Absent:
|
||||
# We have an event ID, but the event was not found in the
|
||||
# datastore. This can happen if a room, or its history, is
|
||||
# purged. State deltas related to the room are left behind, but
|
||||
@@ -413,9 +411,7 @@ class DelayedEventsHandler:
|
||||
NotFoundError: if no matching delayed event could be found.
|
||||
"""
|
||||
assert self._is_master
|
||||
await self._delayed_event_mgmt_ratelimiter.ratelimit(
|
||||
None, request.getClientAddress().host
|
||||
)
|
||||
await self._mgmt_ratelimit(request)
|
||||
await make_deferred_yieldable(self._initialized_from_db)
|
||||
|
||||
next_send_ts = await self._store.cancel_delayed_event(delay_id)
|
||||
@@ -430,9 +426,7 @@ class DelayedEventsHandler:
|
||||
Raises:
|
||||
NotFoundError: if no matching delayed event could be found.
|
||||
"""
|
||||
await self._delayed_event_mgmt_ratelimiter.ratelimit(
|
||||
None, request.getClientAddress().host
|
||||
)
|
||||
await self._mgmt_ratelimit(request)
|
||||
|
||||
# Note: We don't need to wait on `self._initialized_from_db` here as the
|
||||
# events that deals with are already marked as processed.
|
||||
@@ -456,9 +450,7 @@ class DelayedEventsHandler:
|
||||
NotFoundError: if no matching delayed event could be found.
|
||||
"""
|
||||
assert self._is_master
|
||||
await self._delayed_event_mgmt_ratelimiter.ratelimit(
|
||||
None, request.getClientAddress().host
|
||||
)
|
||||
await self._mgmt_ratelimit(request)
|
||||
await make_deferred_yieldable(self._initialized_from_db)
|
||||
|
||||
event, next_send_ts = await self._store.process_target_delayed_event(delay_id)
|
||||
@@ -468,6 +460,19 @@ class DelayedEventsHandler:
|
||||
|
||||
await self._send_event(event)
|
||||
|
||||
async def _mgmt_ratelimit(self, request: SynapseRequest) -> None:
|
||||
"""
|
||||
Ratelimit requests with the `_delayed_event_mgmt_ratelimiter` keyed on the
|
||||
user making the request, or the request's IP address if unauthed.
|
||||
"""
|
||||
if self._auth.has_access_token(request):
|
||||
requester = await self._auth.get_user_by_req(request)
|
||||
key = None
|
||||
else:
|
||||
requester = None
|
||||
key = request.getClientAddress().host
|
||||
await self._delayed_event_mgmt_ratelimiter.ratelimit(requester, key)
|
||||
|
||||
async def _send_on_timeout(self) -> None:
|
||||
self._next_delayed_event_call = None
|
||||
|
||||
@@ -527,10 +532,7 @@ class DelayedEventsHandler:
|
||||
|
||||
async def get_all_for_user(self, requester: Requester) -> list[JsonDict]:
|
||||
"""Return all pending delayed events requested by the given user."""
|
||||
await self._delayed_event_mgmt_ratelimiter.ratelimit(
|
||||
requester,
|
||||
(requester.user.to_string(), requester.device_id),
|
||||
)
|
||||
await self._delayed_event_mgmt_ratelimiter.ratelimit(requester)
|
||||
return await self._store.get_all_delayed_events_for_user(
|
||||
requester.user.localpart
|
||||
)
|
||||
|
||||
@@ -23,8 +23,6 @@ from typing import TYPE_CHECKING, Mapping
|
||||
|
||||
from synapse import event_auth
|
||||
from synapse.api.constants import (
|
||||
CREATOR_POWER_LEVEL,
|
||||
EventContentFields,
|
||||
EventTypes,
|
||||
JoinRules,
|
||||
Membership,
|
||||
@@ -38,6 +36,7 @@ from synapse.event_auth import (
|
||||
)
|
||||
from synapse.events import EventBase
|
||||
from synapse.events.builder import EventBuilder
|
||||
from synapse.handlers.room_member import get_users_which_can_issue_invite
|
||||
from synapse.types import StateMap, StrCollection
|
||||
|
||||
if TYPE_CHECKING:
|
||||
@@ -143,53 +142,20 @@ class EventAuthHandler:
|
||||
Raises:
|
||||
SynapseError if no appropriate user is found.
|
||||
"""
|
||||
create_event_id = current_state_ids[(EventTypes.Create, "")]
|
||||
create_event = await self._store.get_event(create_event_id)
|
||||
power_level_event_id = current_state_ids.get((EventTypes.PowerLevels, ""))
|
||||
invite_level = 0
|
||||
users_default_level = 0
|
||||
if power_level_event_id:
|
||||
power_level_event = await self._store.get_event(power_level_event_id)
|
||||
invite_level = power_level_event.content.get("invite", invite_level)
|
||||
users_default_level = power_level_event.content.get(
|
||||
"users_default", users_default_level
|
||||
)
|
||||
users = power_level_event.content.get("users", {})
|
||||
else:
|
||||
users = {}
|
||||
|
||||
# Find the user with the highest power level (only interested in local
|
||||
# users).
|
||||
user_power_level = 0
|
||||
chosen_user = None
|
||||
local_users_in_room = await self._store.get_local_users_in_room(room_id)
|
||||
if create_event.room_version.msc4289_creator_power_enabled:
|
||||
creators = set(
|
||||
create_event.content.get(EventContentFields.ADDITIONAL_CREATORS, [])
|
||||
)
|
||||
creators.add(create_event.sender)
|
||||
local_creators = creators.intersection(set(local_users_in_room))
|
||||
if len(local_creators) > 0:
|
||||
chosen_user = local_creators.pop() # random creator
|
||||
user_power_level = CREATOR_POWER_LEVEL
|
||||
if chosen_user is None:
|
||||
chosen_user = max(
|
||||
local_users_in_room,
|
||||
key=lambda user: users.get(user, users_default_level),
|
||||
default=None,
|
||||
)
|
||||
# Return the chosen if they can issue invites.
|
||||
if chosen_user:
|
||||
user_power_level = users.get(chosen_user, users_default_level)
|
||||
current_state = await self._store.get_events(current_state_ids.values())
|
||||
auth_events = {
|
||||
state_key: event
|
||||
for state_key, event_id in current_state_ids.items()
|
||||
if (event := current_state.get(event_id)) is not None
|
||||
}
|
||||
|
||||
if chosen_user and user_power_level >= invite_level:
|
||||
logger.debug(
|
||||
"Found a user who can issue invites %s with power level %d >= invite level %d",
|
||||
chosen_user,
|
||||
user_power_level,
|
||||
invite_level,
|
||||
)
|
||||
return chosen_user
|
||||
users_which_can_invite = get_users_which_can_issue_invite(auth_events)
|
||||
local_users_which_can_invite = set(users_which_can_invite).intersection(
|
||||
local_users_in_room
|
||||
)
|
||||
if local_users_which_can_invite:
|
||||
return local_users_which_can_invite.pop()
|
||||
|
||||
# No user was found.
|
||||
raise SynapseError(
|
||||
|
||||
@@ -2294,7 +2294,32 @@ class EventCreationHandler:
|
||||
now = self.clock.time_msec()
|
||||
self._rooms_to_exclude_from_dummy_event_insertion[room_id] = now
|
||||
|
||||
async def _send_dummy_event_for_room(self, room_id: str) -> bool:
|
||||
async def _send_dummy_event_after_room_join(self, room_id: str) -> None:
|
||||
"""
|
||||
Creates and sends a dummy event into the given room, referencing the
|
||||
current forward extremities (via `prev_events`).
|
||||
This should only be triggered when handling a remote join while events
|
||||
were sent during the make_join/send_join handshake. The joining
|
||||
homeserver would otherwise not immediately know to backfill those events
|
||||
and would "miss" them.
|
||||
"""
|
||||
async with self._worker_lock_handler.acquire_read_write_lock(
|
||||
NEW_EVENT_DURING_PURGE_LOCK_NAME, room_id, write=False
|
||||
):
|
||||
dummy_event_sent = await self._send_dummy_event_for_room(
|
||||
room_id, proactively_send=True
|
||||
)
|
||||
|
||||
if not dummy_event_sent:
|
||||
logger.warning(
|
||||
"Failed to send dummy event into room %s after remote join; "
|
||||
"no local user with permission was found",
|
||||
room_id,
|
||||
)
|
||||
|
||||
async def _send_dummy_event_for_room(
|
||||
self, room_id: str, proactively_send: bool = False
|
||||
) -> bool:
|
||||
"""Attempt to send a dummy event for the given room.
|
||||
|
||||
Args:
|
||||
@@ -2326,8 +2351,7 @@ class EventCreationHandler:
|
||||
},
|
||||
)
|
||||
context = await unpersisted_context.persist(event)
|
||||
|
||||
event.internal_metadata.proactively_send = False
|
||||
event.internal_metadata.proactively_send = proactively_send
|
||||
|
||||
# Since this is a dummy-event it is OK if it is sent by a
|
||||
# shadow-banned user.
|
||||
|
||||
@@ -513,7 +513,7 @@ class RoomSummaryHandler:
|
||||
):
|
||||
return None
|
||||
|
||||
room_entry = await self._build_room_entry(room_id, for_federation=bool(origin))
|
||||
room_entry = await self._build_room_entry(room_id)
|
||||
|
||||
# If the room is not a space return just the room information.
|
||||
if room_entry.get("room_type") != RoomTypes.SPACE or not include_children:
|
||||
@@ -769,14 +769,12 @@ class RoomSummaryHandler:
|
||||
# pending invite, etc.
|
||||
return await self._is_local_room_accessible(room_id, requester)
|
||||
|
||||
async def _build_room_entry(self, room_id: str, for_federation: bool) -> JsonDict:
|
||||
async def _build_room_entry(self, room_id: str) -> JsonDict:
|
||||
"""
|
||||
Generate en entry summarising a single room.
|
||||
|
||||
Args:
|
||||
room_id: The room ID to summarize.
|
||||
for_federation: True if this is a summary requested over federation
|
||||
(which includes additional fields).
|
||||
|
||||
Returns:
|
||||
The JSON dictionary for the room.
|
||||
@@ -805,24 +803,30 @@ class RoomSummaryHandler:
|
||||
"encryption": stats.encryption,
|
||||
}
|
||||
|
||||
# Federation requests need to provide additional information so the
|
||||
# requested server is able to filter the response appropriately.
|
||||
if for_federation:
|
||||
current_state_ids = (
|
||||
await self._storage_controllers.state.get_current_state_ids(room_id)
|
||||
# Include allowed_room_ids for rooms with restricted join rules so that
|
||||
# clients can determine which memberships grant access.
|
||||
# Only the join rules event is needed for both has_restricted_join_rules
|
||||
# and get_rooms_that_allow_join, so avoid fetching full state.
|
||||
join_rules_state_ids = (
|
||||
await self._storage_controllers.state.get_current_state_ids(
|
||||
room_id,
|
||||
state_filter=StateFilter.from_types([(EventTypes.JoinRules, "")]),
|
||||
)
|
||||
room_version = await self._store.get_room_version(room_id)
|
||||
)
|
||||
|
||||
if await self._event_auth_handler.has_restricted_join_rules(
|
||||
current_state_ids, room_version
|
||||
):
|
||||
allowed_rooms = (
|
||||
await self._event_auth_handler.get_rooms_that_allow_join(
|
||||
current_state_ids
|
||||
)
|
||||
)
|
||||
if allowed_rooms:
|
||||
entry["allowed_room_ids"] = allowed_rooms
|
||||
try:
|
||||
room_version = await self._store.get_room_version(room_id)
|
||||
except UnsupportedRoomVersionError:
|
||||
room_version = None
|
||||
|
||||
if room_version and await self._event_auth_handler.has_restricted_join_rules(
|
||||
join_rules_state_ids, room_version
|
||||
):
|
||||
allowed_rooms = await self._event_auth_handler.get_rooms_that_allow_join(
|
||||
join_rules_state_ids
|
||||
)
|
||||
if allowed_rooms:
|
||||
entry["allowed_room_ids"] = allowed_rooms
|
||||
|
||||
# Filter out Nones – rather omit the field altogether
|
||||
room_entry = {k: v for k, v in entry.items() if v is not None}
|
||||
@@ -932,7 +936,6 @@ class RoomSummaryHandler:
|
||||
raise NotFoundError("Room not found or is not accessible")
|
||||
|
||||
room = dict(room_entry.room)
|
||||
room.pop("allowed_room_ids", None)
|
||||
|
||||
# If there was a requester, add their membership.
|
||||
# We keep the membership in the local membership table unless the
|
||||
|
||||
@@ -287,7 +287,6 @@ class SlidingSyncHandler:
|
||||
|
||||
lists = interested_rooms.lists
|
||||
relevant_room_map = interested_rooms.relevant_room_map
|
||||
all_rooms = interested_rooms.all_rooms
|
||||
room_membership_for_user_map = interested_rooms.room_membership_for_user_map
|
||||
relevant_rooms_to_send_map = interested_rooms.relevant_rooms_to_send_map
|
||||
|
||||
@@ -328,6 +327,7 @@ class SlidingSyncHandler:
|
||||
actual_lists=lists,
|
||||
previous_connection_state=previous_connection_state,
|
||||
new_connection_state=new_connection_state,
|
||||
all_interested_room_ids=interested_rooms.all_rooms,
|
||||
# We're purposely using `relevant_room_map` instead of
|
||||
# `relevant_rooms_to_send_map` here. This needs to be all room_ids we could
|
||||
# send regardless of whether they have an event update or not. The
|
||||
@@ -350,7 +350,7 @@ class SlidingSyncHandler:
|
||||
if from_token:
|
||||
# The set of rooms that the client (may) care about, but aren't
|
||||
# in any list range (or subscribed to).
|
||||
missing_rooms = all_rooms - relevant_room_map.keys()
|
||||
missing_rooms = interested_rooms.all_rooms - relevant_room_map.keys()
|
||||
|
||||
# We now just go and try fetching any events in the above rooms
|
||||
# to see if anything has happened since the `from_token`.
|
||||
|
||||
@@ -11,7 +11,6 @@
|
||||
# See the GNU Affero General Public License for more details:
|
||||
# <https://www.gnu.org/licenses/agpl-3.0.html>.
|
||||
#
|
||||
|
||||
import itertools
|
||||
import logging
|
||||
from collections import ChainMap
|
||||
@@ -26,11 +25,13 @@ from typing import (
|
||||
|
||||
from typing_extensions import TypeAlias, assert_never
|
||||
|
||||
from synapse.api.constants import AccountDataTypes, EduTypes
|
||||
from synapse.api.constants import AccountDataTypes, EduTypes, StickyEvent
|
||||
from synapse.events.utils import FilteredEvent
|
||||
from synapse.handlers.receipts import ReceiptEventSource
|
||||
from synapse.logging.opentracing import trace
|
||||
from synapse.storage.databases.main.receipts import ReceiptInRoom
|
||||
from synapse.types import (
|
||||
Absent,
|
||||
DeviceListUpdates,
|
||||
JsonMapping,
|
||||
MultiWriterStreamToken,
|
||||
@@ -47,10 +48,12 @@ from synapse.types.handlers.sliding_sync import (
|
||||
SlidingSyncConfig,
|
||||
SlidingSyncResult,
|
||||
)
|
||||
from synapse.types.rest.client import SlidingSyncStickyEventsToken
|
||||
from synapse.util.async_helpers import (
|
||||
concurrently_execute,
|
||||
gather_optional_coroutines,
|
||||
)
|
||||
from synapse.visibility import filter_and_transform_events_for_client
|
||||
|
||||
_ThreadSubscription: TypeAlias = (
|
||||
SlidingSyncResult.Extensions.ThreadSubscriptionsExtension.ThreadSubscription
|
||||
@@ -73,7 +76,10 @@ class SlidingSyncExtensionHandler:
|
||||
self.event_sources = hs.get_event_sources()
|
||||
self.device_handler = hs.get_device_handler()
|
||||
self.push_rules_handler = hs.get_push_rules_handler()
|
||||
self.clock = hs.get_clock()
|
||||
self._storage_controllers = hs.get_storage_controllers()
|
||||
self._enable_thread_subscriptions = hs.config.experimental.msc4306_enabled
|
||||
self._enable_sticky_events = hs.config.experimental.msc4354_enabled
|
||||
|
||||
@trace
|
||||
async def get_extensions_response(
|
||||
@@ -81,6 +87,7 @@ class SlidingSyncExtensionHandler:
|
||||
sync_config: SlidingSyncConfig,
|
||||
previous_connection_state: "PerConnectionState",
|
||||
new_connection_state: "MutablePerConnectionState",
|
||||
all_interested_room_ids: set[str],
|
||||
actual_lists: Mapping[str, SlidingSyncResult.SlidingWindowList],
|
||||
actual_room_ids: set[str],
|
||||
actual_room_response_map: Mapping[str, SlidingSyncResult.RoomResult],
|
||||
@@ -91,9 +98,12 @@ class SlidingSyncExtensionHandler:
|
||||
|
||||
Args:
|
||||
sync_config: Sync configuration
|
||||
new_connection_state: Snapshot of the current per-connection state
|
||||
new_per_connection_state: A mutable copy of the per-connection
|
||||
previous_connection_state: Snapshot of the current per-connection state
|
||||
new_connection_state: A mutable copy of the per-connection
|
||||
state, used to record updates to the state during this request.
|
||||
all_interested_room_ids: The IDs of all rooms that the client is interested in,
|
||||
even if they don't appear in the current limited window.
|
||||
See `SlidingSyncInterestedRooms.all_rooms`.
|
||||
actual_lists: Sliding window API. A map of list key to list results in the
|
||||
Sliding Sync response.
|
||||
actual_room_ids: The actual room IDs in the the Sliding Sync response.
|
||||
@@ -174,6 +184,19 @@ class SlidingSyncExtensionHandler:
|
||||
from_token=from_token,
|
||||
)
|
||||
|
||||
sticky_events_coro = None
|
||||
if (
|
||||
sync_config.extensions.sticky_events is not Absent
|
||||
and self._enable_sticky_events
|
||||
):
|
||||
sticky_events_coro = self.get_sticky_events_extension_response(
|
||||
sync_config=sync_config,
|
||||
sticky_events_request=sync_config.extensions.sticky_events,
|
||||
all_interested_room_ids=all_interested_room_ids,
|
||||
to_token=to_token,
|
||||
from_token=from_token,
|
||||
)
|
||||
|
||||
(
|
||||
to_device_response,
|
||||
e2ee_response,
|
||||
@@ -181,6 +204,7 @@ class SlidingSyncExtensionHandler:
|
||||
receipts_response,
|
||||
typing_response,
|
||||
thread_subs_response,
|
||||
sticky_events_response,
|
||||
) = await gather_optional_coroutines(
|
||||
to_device_coro,
|
||||
e2ee_coro,
|
||||
@@ -188,6 +212,7 @@ class SlidingSyncExtensionHandler:
|
||||
receipts_coro,
|
||||
typing_coro,
|
||||
thread_subs_coro,
|
||||
sticky_events_coro,
|
||||
)
|
||||
|
||||
return SlidingSyncResult.Extensions(
|
||||
@@ -197,6 +222,7 @@ class SlidingSyncExtensionHandler:
|
||||
receipts=receipts_response,
|
||||
typing=typing_response,
|
||||
thread_subscriptions=thread_subs_response,
|
||||
sticky_events=sticky_events_response,
|
||||
)
|
||||
|
||||
def find_relevant_room_ids_for_extension(
|
||||
@@ -967,3 +993,65 @@ class SlidingSyncExtensionHandler:
|
||||
unsubscribed=unsubscribed_threads,
|
||||
prev_batch=prev_batch,
|
||||
)
|
||||
|
||||
async def get_sticky_events_extension_response(
|
||||
self,
|
||||
sync_config: SlidingSyncConfig,
|
||||
sticky_events_request: SlidingSyncConfig.Extensions.StickyEventsExtension,
|
||||
all_interested_room_ids: set[str],
|
||||
to_token: StreamToken,
|
||||
from_token: SlidingSyncStreamToken | None,
|
||||
) -> SlidingSyncResult.Extensions.StickyEventsExtension | None:
|
||||
if not sticky_events_request.enabled:
|
||||
return None
|
||||
now = self.clock.time_msec()
|
||||
# If there is no `since` token specified, start from the beginning of the stream
|
||||
# to make sure the client receives all visible (unexpired) sticky events
|
||||
since_token = sticky_events_request.since or SlidingSyncStickyEventsToken.START
|
||||
(
|
||||
sticky_events_to_id,
|
||||
room_to_event_ids,
|
||||
) = await self.store.get_sticky_events_in_rooms(
|
||||
all_interested_room_ids,
|
||||
from_id=since_token.sticky_events_stream_id,
|
||||
to_id=to_token.sticky_events_key,
|
||||
now=now,
|
||||
limit=min(sticky_events_request.limit, StickyEvent.MAX_EVENTS_IN_SYNC),
|
||||
)
|
||||
# No need to preserve sticky event order here because we will
|
||||
# reassemble it in the right order after.
|
||||
all_sticky_event_ids = {
|
||||
ev_id for evs in room_to_event_ids.values() for ev_id in evs
|
||||
}
|
||||
unfiltered_events = await self.store.get_events_as_list(all_sticky_event_ids)
|
||||
filtered_events = await filter_and_transform_events_for_client(
|
||||
self._storage_controllers,
|
||||
sync_config.user.to_string(),
|
||||
unfiltered_events,
|
||||
# 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(all_sticky_event_ids),
|
||||
)
|
||||
filtered_event_map = {ev.event.event_id: ev for ev in filtered_events}
|
||||
|
||||
room_id_to_sticky_events: dict[str, list[FilteredEvent]] = {}
|
||||
for room_id, sticky_event_ids in room_to_event_ids.items():
|
||||
filtered_events_for_room = [
|
||||
filtered_event_map[event_id]
|
||||
# This reintroduces the correct order
|
||||
# (by the sticky events stream)
|
||||
for event_id in sticky_event_ids
|
||||
if event_id in filtered_event_map
|
||||
]
|
||||
if len(filtered_events_for_room) == 0:
|
||||
continue
|
||||
|
||||
room_id_to_sticky_events[room_id] = filtered_events_for_room
|
||||
|
||||
return SlidingSyncResult.Extensions.StickyEventsExtension(
|
||||
room_id_to_sticky_events=room_id_to_sticky_events,
|
||||
next_batch=SlidingSyncStickyEventsToken(
|
||||
sticky_events_stream_id=sticky_events_to_id
|
||||
),
|
||||
)
|
||||
@@ -52,6 +52,7 @@ from synapse.storage.roommember import (
|
||||
RoomsForUserStateReset,
|
||||
)
|
||||
from synapse.types import (
|
||||
Absent,
|
||||
MutableStateMap,
|
||||
RoomStreamToken,
|
||||
StateMap,
|
||||
@@ -71,7 +72,6 @@ from synapse.types.handlers.sliding_sync import (
|
||||
from synapse.types.state import StateFilter
|
||||
from synapse.util import MutableOverlayMapping
|
||||
from synapse.util.duration import Duration
|
||||
from synapse.util.sentinel import Sentinel
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from synapse.server import HomeServer
|
||||
@@ -112,31 +112,55 @@ class SlidingSyncInterestedRooms:
|
||||
sliding sync request.
|
||||
|
||||
Returned by `compute_interested_rooms`.
|
||||
|
||||
Attributes:
|
||||
lists: A mapping from list name to the list result for the response
|
||||
relevant_room_map: A map from rooms that match the sync request to
|
||||
their room sync config.
|
||||
relevant_rooms_to_send_map: Subset of `relevant_room_map` that
|
||||
includes the rooms that *may* have relevant updates. Rooms not
|
||||
in this map will definitely not have room updates (though
|
||||
extensions may have updates in these rooms).
|
||||
newly_joined_rooms: The set of rooms that were joined in the token range
|
||||
and the user is still joined to at the end of this range.
|
||||
newly_left_rooms: The set of rooms that we left in the token range
|
||||
and are still "leave" at the end of this range.
|
||||
dm_room_ids: The set of rooms the user consider as direct-message (DM) rooms
|
||||
"""
|
||||
|
||||
lists: Mapping[str, SlidingSyncResult.SlidingWindowList]
|
||||
"""
|
||||
A mapping from list name to the list result for the response
|
||||
"""
|
||||
|
||||
relevant_room_map: Mapping[str, RoomSyncConfig]
|
||||
"""
|
||||
A map from rooms that match the sync request to
|
||||
their room sync config.
|
||||
"""
|
||||
|
||||
relevant_rooms_to_send_map: Mapping[str, RoomSyncConfig]
|
||||
"""
|
||||
Subset of `relevant_room_map` that
|
||||
includes the rooms that *may* have relevant updates. Rooms not
|
||||
in this map will definitely not have room updates (though
|
||||
extensions may have updates in these rooms).
|
||||
"""
|
||||
|
||||
all_rooms: set[str]
|
||||
"""
|
||||
The set of room IDs of all rooms that could appear in any list.
|
||||
This set includes rooms that are outside the list ranges.
|
||||
In other words, this is the set of all rooms that the client is
|
||||
_interested_ in (in a pure sense),
|
||||
even if these rooms are omitted from the current window (which
|
||||
is, in a sense, just a computational optimisation).
|
||||
"""
|
||||
|
||||
room_membership_for_user_map: Mapping[str, RoomsForUserType]
|
||||
|
||||
newly_joined_rooms: AbstractSet[str]
|
||||
"""
|
||||
The set of rooms that were joined in the token range
|
||||
and the user is still joined to at the end of this range.
|
||||
"""
|
||||
|
||||
newly_left_rooms: AbstractSet[str]
|
||||
"""
|
||||
The set of rooms that we left in the token range
|
||||
and are still "leave" at the end of this range.
|
||||
"""
|
||||
|
||||
dm_room_ids: AbstractSet[str]
|
||||
"""
|
||||
The set of rooms the user consider as direct-message (DM) rooms
|
||||
"""
|
||||
|
||||
@staticmethod
|
||||
def empty() -> "SlidingSyncInterestedRooms":
|
||||
@@ -1703,10 +1727,8 @@ class SlidingSyncRoomLists:
|
||||
# (applies to invite/knock rooms)
|
||||
rooms_ids_without_stripped_state: set[str] = set()
|
||||
for room_id in room_ids_without_results:
|
||||
stripped_state_map = room_id_to_stripped_state_map.get(
|
||||
room_id, Sentinel.UNSET_SENTINEL
|
||||
)
|
||||
assert stripped_state_map is not Sentinel.UNSET_SENTINEL, (
|
||||
stripped_state_map = room_id_to_stripped_state_map.get(room_id, Absent)
|
||||
assert stripped_state_map is not Absent, (
|
||||
f"Stripped state left unset for room {room_id}. "
|
||||
+ "Make sure you're calling `_bulk_get_stripped_state_for_rooms_from_sync_room_map(...)` "
|
||||
+ "with that room_id. (this is a problem with Synapse itself)"
|
||||
|
||||
@@ -142,6 +142,8 @@ from synapse.storage.background_updates import (
|
||||
from synapse.storage.database import DatabasePool, LoggingTransaction
|
||||
from synapse.storage.databases.main.roommember import ProfileInfo
|
||||
from synapse.types import (
|
||||
Absent,
|
||||
AbsentType,
|
||||
DomainSpecificString,
|
||||
JsonDict,
|
||||
JsonMapping,
|
||||
@@ -160,7 +162,6 @@ from synapse.util.caches.descriptors import CachedFunction, cached as _cached
|
||||
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`
|
||||
@@ -1990,7 +1991,7 @@ class ModuleApi:
|
||||
self,
|
||||
user_id: UserID,
|
||||
new_displayname: str,
|
||||
deactivation: bool | Sentinel = Sentinel.UNSET_SENTINEL,
|
||||
deactivation: bool | AbsentType = Absent,
|
||||
) -> None:
|
||||
"""Sets a user's display name.
|
||||
|
||||
@@ -2020,7 +2021,7 @@ class ModuleApi:
|
||||
"""
|
||||
requester = create_requester(user_id)
|
||||
|
||||
if deactivation is not Sentinel.UNSET_SENTINEL:
|
||||
if deactivation is not Absent:
|
||||
logger.error(
|
||||
"Deprecated `deactivation` parameter passed to `set_displayname` Module API (value: %r). This will break in 2027.",
|
||||
deactivation,
|
||||
|
||||
@@ -57,6 +57,7 @@ from synapse.rest.client import (
|
||||
relations,
|
||||
rendezvous,
|
||||
reporting,
|
||||
retention,
|
||||
room,
|
||||
room_keys,
|
||||
room_upgrade_rest_servlet,
|
||||
@@ -107,6 +108,7 @@ CLIENT_SERVLET_FUNCTIONS: tuple[RegisterServletsFunc, ...] = (
|
||||
tags.register_servlets,
|
||||
account_data.register_servlets,
|
||||
reporting.register_servlets,
|
||||
retention.register_servlets,
|
||||
openid.register_servlets,
|
||||
notifications.register_servlets,
|
||||
devices.register_servlets,
|
||||
|
||||
@@ -0,0 +1,102 @@
|
||||
#
|
||||
# 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:
|
||||
# <https://www.gnu.org/licenses/agpl-3.0.html>.
|
||||
|
||||
from http import HTTPStatus
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from typing_extensions import TypedDict
|
||||
|
||||
from synapse.http.server import HttpServer
|
||||
from synapse.http.servlet import RestServlet
|
||||
from synapse.http.site import SynapseRequest
|
||||
|
||||
from ._base import client_patterns
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from synapse.server import HomeServer
|
||||
|
||||
|
||||
class RetentionPolicyDict(TypedDict, total=False):
|
||||
min_lifetime: int
|
||||
max_lifetime: int
|
||||
|
||||
|
||||
class LifetimeBoundsDict(TypedDict, total=False):
|
||||
min: int
|
||||
max: int
|
||||
|
||||
|
||||
class RetentionLimitsDict(TypedDict, total=False):
|
||||
max_lifetime: LifetimeBoundsDict
|
||||
|
||||
|
||||
class RetentionConfigurationResponse(TypedDict):
|
||||
policies: dict[str, RetentionPolicyDict]
|
||||
limits: RetentionLimitsDict
|
||||
|
||||
|
||||
class RetentionConfigurationServlet(RestServlet):
|
||||
"""Implements MSC1763: /_matrix/client/unstable/org.matrix.msc1763/retention/configuration"""
|
||||
|
||||
PATTERNS = client_patterns(
|
||||
"/org.matrix.msc1763/retention/configuration$",
|
||||
releases=[],
|
||||
v1=False,
|
||||
unstable=True,
|
||||
)
|
||||
CATEGORY = "Client API requests"
|
||||
|
||||
def __init__(self, hs: "HomeServer"):
|
||||
super().__init__()
|
||||
self.auth = hs.get_auth()
|
||||
self._retention_config = hs.config.retention
|
||||
|
||||
async def on_GET(
|
||||
self, request: SynapseRequest
|
||||
) -> tuple[int, RetentionConfigurationResponse]:
|
||||
await self.auth.get_user_by_req(request)
|
||||
|
||||
default_policy: RetentionPolicyDict = {}
|
||||
if self._retention_config.retention_default_min_lifetime is not None:
|
||||
default_policy["min_lifetime"] = (
|
||||
self._retention_config.retention_default_min_lifetime
|
||||
)
|
||||
if self._retention_config.retention_default_max_lifetime is not None:
|
||||
default_policy["max_lifetime"] = (
|
||||
self._retention_config.retention_default_max_lifetime
|
||||
)
|
||||
|
||||
max_lifetime_limits: LifetimeBoundsDict = {}
|
||||
if self._retention_config.retention_allowed_lifetime_min is not None:
|
||||
max_lifetime_limits["min"] = (
|
||||
self._retention_config.retention_allowed_lifetime_min
|
||||
)
|
||||
if self._retention_config.retention_allowed_lifetime_max is not None:
|
||||
max_lifetime_limits["max"] = (
|
||||
self._retention_config.retention_allowed_lifetime_max
|
||||
)
|
||||
|
||||
limits: RetentionLimitsDict = {}
|
||||
if max_lifetime_limits:
|
||||
limits["max_lifetime"] = max_lifetime_limits
|
||||
|
||||
policies: dict[str, RetentionPolicyDict] = {}
|
||||
if default_policy:
|
||||
policies["*"] = default_policy
|
||||
|
||||
return HTTPStatus.OK, {"policies": policies, "limits": limits}
|
||||
|
||||
|
||||
def register_servlets(hs: "HomeServer", http_server: HttpServer) -> None:
|
||||
if hs.config.retention.retention_enabled and hs.config.experimental.msc1763_enabled:
|
||||
RetentionConfigurationServlet(hs).register(http_server)
|
||||
@@ -20,7 +20,7 @@
|
||||
#
|
||||
import logging
|
||||
from collections import defaultdict
|
||||
from typing import TYPE_CHECKING, Any, Mapping
|
||||
from typing import TYPE_CHECKING, Any, Literal, Mapping
|
||||
|
||||
import attr
|
||||
|
||||
@@ -671,6 +671,7 @@ class SlidingSyncRestServlet(RestServlet):
|
||||
- receipts (MSC3960)
|
||||
- account data (MSC3959)
|
||||
- thread subscriptions (MSC4308)
|
||||
- sticky events (MSC4354)
|
||||
|
||||
Request query parameters:
|
||||
timeout: How long to wait for new events in milliseconds.
|
||||
@@ -894,7 +895,7 @@ class SlidingSyncRestServlet(RestServlet):
|
||||
requester, sliding_sync_result.rooms
|
||||
)
|
||||
response["extensions"] = await self.encode_extensions(
|
||||
requester, sliding_sync_result.extensions
|
||||
requester, sliding_sync_result.extensions, sliding_sync_result.rooms
|
||||
)
|
||||
|
||||
return response
|
||||
@@ -1044,8 +1045,18 @@ class SlidingSyncRestServlet(RestServlet):
|
||||
|
||||
@trace_with_opname("sliding_sync.encode_extensions")
|
||||
async def encode_extensions(
|
||||
self, requester: Requester, extensions: SlidingSyncResult.Extensions
|
||||
self,
|
||||
requester: Requester,
|
||||
extensions: SlidingSyncResult.Extensions,
|
||||
ref_rooms_results: Mapping[str, SlidingSyncResult.RoomResult],
|
||||
) -> JsonDict:
|
||||
"""
|
||||
Args:
|
||||
ref_rooms_results:
|
||||
Map of room ID -> RoomResult that was serialised as the `room` section
|
||||
of the Sliding Sync response.
|
||||
Will not be mutated, only used for reading.
|
||||
"""
|
||||
serialized_extensions: JsonDict = {}
|
||||
|
||||
if extensions.to_device is not None:
|
||||
@@ -1114,8 +1125,80 @@ class SlidingSyncRestServlet(RestServlet):
|
||||
_serialise_thread_subscriptions(extensions.thread_subscriptions)
|
||||
)
|
||||
|
||||
if extensions.sticky_events:
|
||||
serialized_extensions[
|
||||
"org.matrix.msc4354.sticky_events"
|
||||
] = await self._serialise_sticky_events(
|
||||
requester, extensions.sticky_events, ref_rooms_results
|
||||
)
|
||||
|
||||
return serialized_extensions
|
||||
|
||||
async def _serialise_sticky_events(
|
||||
self,
|
||||
requester: Requester,
|
||||
sticky_events: SlidingSyncResult.Extensions.StickyEventsExtension,
|
||||
ref_rooms_results: Mapping[str, SlidingSyncResult.RoomResult],
|
||||
) -> JsonDict:
|
||||
"""
|
||||
Serialise the sticky events extension response.
|
||||
|
||||
This includes deduplicating by filtering out sticky events
|
||||
from this extension that already appeared in the timeline
|
||||
section.
|
||||
|
||||
Args:
|
||||
ref_rooms_results:
|
||||
Map of room ID -> RoomResult that was serialised as the `room` section
|
||||
of the Sliding Sync response.
|
||||
Will not be mutated, only used for reading.
|
||||
"""
|
||||
|
||||
time_now = self.clock.time_msec()
|
||||
# Same as SSS timelines.
|
||||
#
|
||||
serialize_options = SerializeEventConfig(
|
||||
event_format=format_event_for_client_v2_without_room_id,
|
||||
requester=requester,
|
||||
)
|
||||
|
||||
rooms_out: dict[str, dict[Literal["events"], list[JsonDict]]] = {}
|
||||
for (
|
||||
room_id,
|
||||
possibly_duplicated_sticky_events,
|
||||
) in sticky_events.room_id_to_sticky_events.items():
|
||||
# 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.
|
||||
room_result = ref_rooms_results.get(room_id)
|
||||
if room_result is None:
|
||||
# Nothing to deduplicate
|
||||
sticky_events_to_write = possibly_duplicated_sticky_events
|
||||
else:
|
||||
sent_event_ids_in_room_section = {
|
||||
ev.event.event_id for ev in room_result.timeline_events
|
||||
}
|
||||
sticky_events_to_write = [
|
||||
ev
|
||||
for ev in possibly_duplicated_sticky_events
|
||||
if ev.event.event_id not in sent_event_ids_in_room_section
|
||||
]
|
||||
rooms_out[room_id] = {
|
||||
"events": await self.event_serializer.serialize_events(
|
||||
sticky_events_to_write, time_now, config=serialize_options
|
||||
)
|
||||
}
|
||||
|
||||
return {
|
||||
"rooms": rooms_out,
|
||||
"next_batch": sticky_events.next_batch.serialise(),
|
||||
}
|
||||
|
||||
|
||||
def _serialise_thread_subscriptions(
|
||||
thread_subscriptions: SlidingSyncResult.Extensions.ThreadSubscriptionsExtension,
|
||||
|
||||
@@ -1879,7 +1879,7 @@ class DatabasePool:
|
||||
if allow_none:
|
||||
return None
|
||||
else:
|
||||
raise StoreError(404, "No row found")
|
||||
raise StoreError(404, f"No row found ({table})")
|
||||
|
||||
@staticmethod
|
||||
def simple_select_onecol_txn(
|
||||
|
||||
@@ -1509,6 +1509,43 @@ class EventPushActionsWorkerStore(ReceiptsWorkerStore, StreamWorkerStore, SQLBas
|
||||
"last_receipt_stream_ordering": stream_ordering,
|
||||
},
|
||||
)
|
||||
# If no summary row exists yet for a thread that has pending push
|
||||
# actions (room active but not yet through a rotation cycle), the
|
||||
# UPDATE above is a silent no-op for that thread and
|
||||
# last_receipt_stream_ordering is never persisted.
|
||||
# _rotate_notifs_before_txn would then INSERT the row with
|
||||
# last_receipt_stream_ordering=NULL, causing the badge query to
|
||||
# include every event before the receipt as unread. Pre-populate
|
||||
# rows for every thread with pending push actions so rotation
|
||||
# only counts events that arrive after this receipt.
|
||||
txn.execute(
|
||||
"""
|
||||
SELECT DISTINCT thread_id
|
||||
FROM event_push_actions
|
||||
WHERE user_id = ? AND room_id = ?
|
||||
""",
|
||||
(user_id, room_id),
|
||||
)
|
||||
pending_thread_ids = [row[0] for row in txn]
|
||||
self.db_pool.simple_upsert_many_txn(
|
||||
txn,
|
||||
table="event_push_summary",
|
||||
key_names=("user_id", "room_id", "thread_id"),
|
||||
key_values=[
|
||||
(user_id, room_id, pending_thread_id)
|
||||
for pending_thread_id in pending_thread_ids
|
||||
],
|
||||
value_names=(
|
||||
"notif_count",
|
||||
"unread_count",
|
||||
"stream_ordering",
|
||||
"last_receipt_stream_ordering",
|
||||
),
|
||||
value_values=[
|
||||
(0, 0, old_rotate_stream_ordering, stream_ordering)
|
||||
for _ in pending_thread_ids
|
||||
],
|
||||
)
|
||||
|
||||
# For a threaded receipt, we *always* want to update that receipt,
|
||||
# event if there are no new notifications in that thread. This ensures
|
||||
@@ -1517,8 +1554,10 @@ class EventPushActionsWorkerStore(ReceiptsWorkerStore, StreamWorkerStore, SQLBas
|
||||
unread_counts = [(0, 0, thread_id)]
|
||||
|
||||
# Then any updated threads get their notification count and unread
|
||||
# count updated.
|
||||
self.db_pool.simple_update_many_txn(
|
||||
# count updated. Use upsert so that a row is created if none exists
|
||||
# yet (same race as the unthreaded case above: without this, rotation
|
||||
# would INSERT with last_receipt_stream_ordering=NULL).
|
||||
self.db_pool.simple_upsert_many_txn(
|
||||
txn,
|
||||
table="event_push_summary",
|
||||
key_names=("room_id", "user_id", "thread_id"),
|
||||
|
||||
@@ -210,7 +210,7 @@ class PurgeEventsStore(StateGroupWorkerStore, CacheInvalidationWorkerStore):
|
||||
should_delete_expr = "state_events.state_key IS NULL"
|
||||
should_delete_params: tuple[Any, ...] = ()
|
||||
if not delete_local_events:
|
||||
should_delete_expr += " AND event_id NOT LIKE ?"
|
||||
should_delete_expr += " AND sender NOT LIKE ?"
|
||||
|
||||
# We include the parameter twice since we use the expression twice
|
||||
should_delete_params += ("%:" + self.hs.hostname, "%:" + self.hs.hostname)
|
||||
@@ -326,6 +326,65 @@ class PurgeEventsStore(StateGroupWorkerStore, CacheInvalidationWorkerStore):
|
||||
")" % (table,)
|
||||
)
|
||||
|
||||
# Some of the `event_push_actions` we're about to delete may have already
|
||||
# been rotated into the aggregate `event_push_summary` counts. Deleting
|
||||
# the rows without adjusting those counts would leave the summary
|
||||
# over-counting, inflating users' notification counts. So first work out
|
||||
# how much of each summary is attributable to the events being deleted and
|
||||
# decrement it.
|
||||
#
|
||||
# We only count rows that have already been rotated into the summary:
|
||||
# those at or before the rotated-up-to position
|
||||
# (`event_push_summary_stream_ordering`) and after the receipt used to
|
||||
# compute the summary. Rows beyond that position aren't in the summary
|
||||
# yet (they're still counted live from `event_push_actions`), so deleting
|
||||
# them needs no adjustment here. This mirrors how rotation counts them in
|
||||
# `_rotate_notifs_before_txn`.
|
||||
logger.info("[purge] adjusting event_push_summary for deleted events")
|
||||
txn.execute(
|
||||
"""
|
||||
SELECT epa.user_id, epa.thread_id,
|
||||
COUNT(CASE WHEN epa.notif = 1 THEN 1 END),
|
||||
COUNT(CASE WHEN epa.unread = 1 THEN 1 END)
|
||||
FROM event_push_actions AS epa
|
||||
INNER JOIN event_push_summary AS eps USING (user_id, room_id, thread_id)
|
||||
WHERE epa.room_id = ?
|
||||
AND epa.event_id IN (
|
||||
SELECT event_id FROM events_to_purge WHERE should_delete
|
||||
)
|
||||
AND epa.stream_ordering <= (
|
||||
SELECT stream_ordering FROM event_push_summary_stream_ordering
|
||||
)
|
||||
AND (
|
||||
eps.last_receipt_stream_ordering IS NULL
|
||||
OR epa.stream_ordering > eps.last_receipt_stream_ordering
|
||||
)
|
||||
GROUP BY epa.user_id, epa.thread_id
|
||||
""",
|
||||
(room_id,),
|
||||
)
|
||||
summary_decrements = cast(list[tuple[str, str, int, int]], txn.fetchall())
|
||||
|
||||
# `unread_count` is nullable, so `COALESCE` it before subtracting (else
|
||||
# the result would be NULL). Clamp both counts at 0 via `GREATEST`/`MAX`
|
||||
# to guard against ever driving a count negative if the summary is
|
||||
# somehow out of sync with `event_push_actions`.
|
||||
greatest_func = (
|
||||
"GREATEST" if isinstance(self.database_engine, PostgresEngine) else "MAX"
|
||||
)
|
||||
txn.execute_batch(
|
||||
f"""
|
||||
UPDATE event_push_summary
|
||||
SET notif_count = {greatest_func}(notif_count - ?, 0),
|
||||
unread_count = {greatest_func}(COALESCE(unread_count, 0) - ?, 0)
|
||||
WHERE room_id = ? AND user_id = ? AND thread_id = ?
|
||||
""",
|
||||
[
|
||||
(notif_count, unread_count, room_id, user_id, thread_id)
|
||||
for user_id, thread_id, notif_count, unread_count in summary_decrements
|
||||
],
|
||||
)
|
||||
|
||||
# event_push_actions lacks an index on event_id, and has one on
|
||||
# (room_id, event_id) instead.
|
||||
for table in ("event_push_actions",):
|
||||
|
||||
@@ -27,8 +27,10 @@ from enum import Enum
|
||||
from typing import (
|
||||
TYPE_CHECKING,
|
||||
AbstractSet,
|
||||
Annotated,
|
||||
Any,
|
||||
ClassVar,
|
||||
Final,
|
||||
Literal,
|
||||
Mapping,
|
||||
Match,
|
||||
@@ -41,8 +43,12 @@ from typing import (
|
||||
overload,
|
||||
)
|
||||
|
||||
import annotated_types
|
||||
import attr
|
||||
import pydantic_core.core_schema
|
||||
from immutabledict import immutabledict
|
||||
from pydantic import GetCoreSchemaHandler, StrictInt
|
||||
from pydantic_core import CoreSchema
|
||||
from signedjson.key import decode_verify_key_bytes
|
||||
from signedjson.types import VerifyKey
|
||||
from typing_extensions import Self
|
||||
@@ -109,6 +115,110 @@ StrCollection = tuple[str, ...] | list[str] | AbstractSet[str]
|
||||
StrSequence = tuple[str, ...] | list[str]
|
||||
|
||||
|
||||
class AbsentType(Enum):
|
||||
"""
|
||||
Type of a sentinel to use as an alternative to `None`
|
||||
for when we really mean 'absent' and not JSON null.
|
||||
|
||||
Generally suitable for distinguishing a default state from user-suppliable values.
|
||||
Has no meaning on its own.
|
||||
|
||||
It is falsy (like None is), so shorthand forms like `x or 0` can be used.
|
||||
"""
|
||||
|
||||
# Making this an Enum member makes this compatible with type narrowing,
|
||||
# meaning `x is not Absent` will narrow `x: int | AbsentType` to `x: int` etc.
|
||||
_Absent = object()
|
||||
|
||||
@classmethod
|
||||
def __get_pydantic_core_schema__(
|
||||
cls, source_type: object, handler: GetCoreSchemaHandler
|
||||
) -> CoreSchema:
|
||||
"""
|
||||
This function is checked for and used by Pydantic when
|
||||
attempting to deserialise/validate a field of this type.
|
||||
|
||||
As the `Absent` type has no valid value when deserialising
|
||||
from JSON (as that's the point; `Absent` is a marker representing
|
||||
a lack of any JSON value), we always reject any value.
|
||||
Instead of deserialising from this type, we rely on the struct class
|
||||
we are in having field defaults that provide an `Absent`, which does not
|
||||
go through the JSON validation.
|
||||
|
||||
When validating Python, we accept the absent marker itself.
|
||||
"""
|
||||
|
||||
def _reject_from_json(v: object) -> "AbsentType":
|
||||
"""
|
||||
Reject the JSON value, no matter what it is, since absent values
|
||||
are meant to be ... absent, thus have nothing they can be deserialised
|
||||
from.
|
||||
"""
|
||||
raise ValueError("AbsentType cannot be deserialized from JSON")
|
||||
|
||||
# `json_or_python_schema` wrapper needed for Pydantic < 2.10
|
||||
# but can be replaced with just the `is_instance_schema` after that version.
|
||||
return pydantic_core.core_schema.json_or_python_schema(
|
||||
json_schema=pydantic_core.core_schema.no_info_plain_validator_function(
|
||||
_reject_from_json
|
||||
),
|
||||
python_schema=pydantic_core.core_schema.is_instance_schema(cls),
|
||||
)
|
||||
|
||||
def __copy__(self) -> "AbsentType":
|
||||
"""
|
||||
Copy implementation used by `copy.copy()`.
|
||||
Always use the same instance.
|
||||
|
||||
Without this and the deep version `__deepcopy__`,
|
||||
`copy.copy(Absent)` on Python 3.10 (olddeps)
|
||||
had a problem where it tried to construct a new Absent
|
||||
as part of a deepcopy operation and resulted in:
|
||||
ValueError: <object object at 0x7f64b3b6d930> is not a valid AbsentType
|
||||
"""
|
||||
return self
|
||||
|
||||
def __deepcopy__(self, memo: object) -> "AbsentType":
|
||||
"""
|
||||
Copy implementation used by `copy.deepcopy()`.
|
||||
Always use the same instance.
|
||||
"""
|
||||
return self
|
||||
|
||||
def __bool__(self) -> Literal[False]:
|
||||
return False
|
||||
|
||||
def __str__(self) -> str:
|
||||
return "Absent"
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return "Absent"
|
||||
|
||||
|
||||
Absent: Final = AbsentType._Absent
|
||||
"""
|
||||
Sentinel to use as an alternative to `None`
|
||||
for when we really mean 'absent' and not JSON null.
|
||||
|
||||
Generally suitable for distinguishing a default state from user-suppliable values.
|
||||
Has no meaning on its own.
|
||||
|
||||
It is falsy (like None is), so shorthand forms like `x or 0` can be used.
|
||||
|
||||
(Previously known as `Sentinel.UNSET_SENTINEL`.)
|
||||
"""
|
||||
|
||||
|
||||
NonNegativeStrictInt = Annotated[StrictInt, annotated_types.Ge(0)]
|
||||
"""A strict integer that must be greater than or equal to zero.
|
||||
|
||||
Should be preferred in place of Pydantic's own (lax) NonNegativeInt,
|
||||
which will coerce strings to integers in a way that does not agree with
|
||||
the Matrix specification (and would risk backing us into a backward compatibility
|
||||
hole where we had to support input forms we didn't intend).
|
||||
"""
|
||||
|
||||
|
||||
# Note that this seems to require inheriting *directly* from Interface in order
|
||||
# for mypy-zope to realize it is an interface.
|
||||
class ISynapseThreadlessReactor(
|
||||
|
||||
@@ -48,7 +48,7 @@ from synapse.types import (
|
||||
ThreadSubscriptionsToken,
|
||||
UserID,
|
||||
)
|
||||
from synapse.types.rest.client import SlidingSyncBody
|
||||
from synapse.types.rest.client import SlidingSyncBody, SlidingSyncStickyEventsToken
|
||||
from synapse.util.clock import Clock
|
||||
from synapse.util.duration import Duration
|
||||
|
||||
@@ -424,12 +424,31 @@ class SlidingSyncResult:
|
||||
or bool(self.prev_batch)
|
||||
)
|
||||
|
||||
@attr.s(slots=True, frozen=True, auto_attribs=True)
|
||||
class StickyEventsExtension:
|
||||
"""The Sticky Events extension (MSC4354)
|
||||
|
||||
Attributes:
|
||||
room_id_to_sticky_events: map (room_id -> [unexpired_sticky_events])
|
||||
The events are ordered by the sticky events stream.
|
||||
|
||||
The events haven't yet been deduplicated to remove
|
||||
events that also appear in the timeline.
|
||||
"""
|
||||
|
||||
room_id_to_sticky_events: Mapping[str, list[FilteredEvent]]
|
||||
next_batch: SlidingSyncStickyEventsToken
|
||||
|
||||
def __bool__(self) -> bool:
|
||||
return bool(self.room_id_to_sticky_events)
|
||||
|
||||
to_device: ToDeviceExtension | None = None
|
||||
e2ee: E2eeExtension | None = None
|
||||
account_data: AccountDataExtension | None = None
|
||||
receipts: ReceiptsExtension | None = None
|
||||
typing: TypingExtension | None = None
|
||||
thread_subscriptions: ThreadSubscriptionsExtension | None = None
|
||||
sticky_events: StickyEventsExtension | None = None
|
||||
|
||||
def __bool__(self) -> bool:
|
||||
"""Are there any updates that should be returned immediately to
|
||||
@@ -441,6 +460,7 @@ class SlidingSyncResult:
|
||||
or self.receipts
|
||||
or self.typing
|
||||
or self.thread_subscriptions
|
||||
or self.sticky_events
|
||||
)
|
||||
|
||||
next_pos: SlidingSyncStreamToken
|
||||
|
||||
@@ -18,9 +18,14 @@
|
||||
# [This file includes modifications made by New Vector Limited]
|
||||
#
|
||||
#
|
||||
import re
|
||||
from typing import ClassVar
|
||||
|
||||
import pydantic_core.core_schema
|
||||
from pydantic import (
|
||||
ConfigDict,
|
||||
Field,
|
||||
GetCoreSchemaHandler,
|
||||
StrictBool,
|
||||
StrictInt,
|
||||
StrictStr,
|
||||
@@ -28,9 +33,10 @@ from pydantic import (
|
||||
field_validator,
|
||||
model_validator,
|
||||
)
|
||||
from pydantic_core import PydanticCustomError
|
||||
from pydantic_core import CoreSchema, PydanticCustomError
|
||||
from typing_extensions import Annotated, Self
|
||||
|
||||
from synapse.types import Absent, AbsentType, NonNegativeStrictInt
|
||||
from synapse.types.rest import RequestBodyModel
|
||||
from synapse.util.threepids import validate_email
|
||||
|
||||
@@ -107,6 +113,82 @@ class MsisdnRequestTokenBody(ThreepidRequestTokenBody):
|
||||
phone_number: StrictStr
|
||||
|
||||
|
||||
class SlidingSyncStickyEventsToken:
|
||||
"""
|
||||
A token returned by `next_batch` of the MSC4354 Sticky Events extension to Sliding Sync
|
||||
and then accepted as the `since` parameter in the requests of the same extension.
|
||||
|
||||
Current format:
|
||||
SlidingSyncStickyEventsToken ::= 'sticky_' DIGIT+
|
||||
DIGIT ::= '0'-'9'
|
||||
|
||||
The `sticky_` prefix allows us to make sure it's not swapped for another token
|
||||
or to evolve the type of token accepted with backwards compatibility in the future.
|
||||
"""
|
||||
|
||||
PATTERN = re.compile(r"^sticky_([0-9]+)$")
|
||||
START: ClassVar["SlidingSyncStickyEventsToken"]
|
||||
|
||||
def __init__(self, *, sticky_events_stream_id: int) -> None:
|
||||
# FIXME: We should use MultiWriterStreamToken here
|
||||
# Track: https://github.com/element-hq/synapse/issues/19661
|
||||
self.sticky_events_stream_id = sticky_events_stream_id
|
||||
|
||||
@classmethod
|
||||
def __get_pydantic_core_schema__(
|
||||
cls, source_type: object, handler: GetCoreSchemaHandler
|
||||
) -> CoreSchema:
|
||||
"""
|
||||
This function is checked for and used by Pydantic when
|
||||
attempting to deserialise/validate a field of this type.
|
||||
|
||||
This returns a schema that will parse a string into an
|
||||
instance of `SlidingSyncStickyEventsToken`.
|
||||
"""
|
||||
|
||||
return pydantic_core.core_schema.no_info_plain_validator_function(
|
||||
cls._validate,
|
||||
serialization=pydantic_core.core_schema.plain_serializer_function_ser_schema(
|
||||
cls.serialise,
|
||||
info_arg=False,
|
||||
),
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def _validate(cls, v: object) -> Self:
|
||||
"""
|
||||
Create an instance from serialised string form.
|
||||
|
||||
The inverse of `serialise`.
|
||||
"""
|
||||
if isinstance(v, cls):
|
||||
return v
|
||||
if isinstance(v, str):
|
||||
match = cls.PATTERN.match(v)
|
||||
if match is None:
|
||||
raise ValueError(f"Invalid SlidingSyncStickyEventsToken format: {v!r}")
|
||||
return cls(sticky_events_stream_id=int(match.group(1)))
|
||||
raise ValueError(f"Cannot parse SlidingSyncStickyEventsToken from {type(v)}")
|
||||
|
||||
def serialise(self) -> str:
|
||||
"""
|
||||
Convert this instance to string.
|
||||
|
||||
The inverse of `_validate`.
|
||||
"""
|
||||
return f"sticky_{self.sticky_events_stream_id}"
|
||||
|
||||
def __repr__(self) -> str:
|
||||
# Use the serialised form as debug output.
|
||||
return self.serialise()
|
||||
|
||||
|
||||
# Starting reading a stream at 0 ensures all stream fact rows will be read
|
||||
SlidingSyncStickyEventsToken.START = SlidingSyncStickyEventsToken(
|
||||
sticky_events_stream_id=0
|
||||
)
|
||||
|
||||
|
||||
class SlidingSyncBody(RequestBodyModel):
|
||||
"""
|
||||
Sliding Sync API request body.
|
||||
@@ -383,6 +465,19 @@ class SlidingSyncBody(RequestBodyModel):
|
||||
enabled: StrictBool | None = False
|
||||
limit: StrictInt = 100
|
||||
|
||||
class StickyEventsExtension(RequestBodyModel):
|
||||
"""The Sticky Events extension (MSC4354)
|
||||
|
||||
Attributes:
|
||||
enabled
|
||||
limit: maximum number of sticky events to return in the extension (default 100)
|
||||
since: either a string with the Sticky Events since token or absent
|
||||
"""
|
||||
|
||||
enabled: StrictBool = False
|
||||
limit: NonNegativeStrictInt = 100
|
||||
since: SlidingSyncStickyEventsToken | AbsentType = Absent
|
||||
|
||||
to_device: ToDeviceExtension | None = None
|
||||
e2ee: E2eeExtension | None = None
|
||||
account_data: AccountDataExtension | None = None
|
||||
@@ -391,6 +486,9 @@ class SlidingSyncBody(RequestBodyModel):
|
||||
thread_subscriptions: ThreadSubscriptionsExtension | None = Field(
|
||||
None, alias="io.element.msc4308.thread_subscriptions"
|
||||
)
|
||||
sticky_events: StickyEventsExtension | AbsentType = Field(
|
||||
Absent, alias="org.matrix.msc4354.sticky_events"
|
||||
)
|
||||
|
||||
conn_id: StrictStr | None = None
|
||||
lists: (
|
||||
|
||||
@@ -98,6 +98,8 @@ class ObservableDeferred(Generic[_T], AbstractObservableDeferred[_T]):
|
||||
deferred.
|
||||
|
||||
If consumeErrors is true errors will be captured from the origin deferred.
|
||||
In that case, errors will NOT be propagated onto any errbacks added to this
|
||||
`ObservableDeferred`; instead the success callbacks will be called with `None`.
|
||||
|
||||
Cancelling or otherwise resolving an observer will not affect the original
|
||||
ObservableDeferred.
|
||||
@@ -388,6 +390,7 @@ T3 = TypeVar("T3")
|
||||
T4 = TypeVar("T4")
|
||||
T5 = TypeVar("T5")
|
||||
T6 = TypeVar("T6")
|
||||
T7 = TypeVar("T7")
|
||||
|
||||
|
||||
@overload
|
||||
@@ -517,6 +520,30 @@ async def gather_optional_coroutines(
|
||||
) -> tuple[T1 | None, T2 | None, T3 | None, T4 | None, T5 | None, T6 | None]: ...
|
||||
|
||||
|
||||
@overload
|
||||
async def gather_optional_coroutines(
|
||||
*coroutines: Unpack[
|
||||
tuple[
|
||||
Coroutine[Any, Any, T1] | None,
|
||||
Coroutine[Any, Any, T2] | None,
|
||||
Coroutine[Any, Any, T3] | None,
|
||||
Coroutine[Any, Any, T4] | None,
|
||||
Coroutine[Any, Any, T5] | None,
|
||||
Coroutine[Any, Any, T6] | None,
|
||||
Coroutine[Any, Any, T7] | None,
|
||||
]
|
||||
],
|
||||
) -> tuple[
|
||||
T1 | None,
|
||||
T2 | None,
|
||||
T3 | None,
|
||||
T4 | None,
|
||||
T5 | None,
|
||||
T6 | None,
|
||||
T7 | None,
|
||||
]: ...
|
||||
|
||||
|
||||
async def gather_optional_coroutines(
|
||||
*coroutines: Unpack[tuple[Coroutine[Any, Any, T1] | None, ...]],
|
||||
) -> tuple[T1 | None, ...]:
|
||||
|
||||
@@ -32,6 +32,7 @@ from typing import (
|
||||
import attr
|
||||
|
||||
from twisted.internet import defer
|
||||
from twisted.python.failure import Failure
|
||||
|
||||
from synapse.logging.context import make_deferred_yieldable, run_in_background
|
||||
from synapse.logging.opentracing import (
|
||||
@@ -226,14 +227,19 @@ class ResponseCache(Generic[KV]):
|
||||
Returns:
|
||||
The cache entry object.
|
||||
"""
|
||||
result = ObservableDeferred(deferred, consumeErrors=True)
|
||||
result = ObservableDeferred(
|
||||
deferred,
|
||||
# We set `consumeErrors=False` as we want to handle errors ourselves (`on_fail`) instead of
|
||||
# replacing them with a `None` successful result that would go to `on_succeed`
|
||||
consumeErrors=False,
|
||||
)
|
||||
key = context.cache_key
|
||||
entry = ResponseCacheEntry(
|
||||
result, opentracing_span_context, cancellable=cancellable
|
||||
)
|
||||
self._result_cache[key] = entry
|
||||
|
||||
def on_complete(r: RV) -> RV:
|
||||
def on_succeed(r: RV) -> RV:
|
||||
# if this cache has a non-zero timeout, and the callback has not cleared
|
||||
# the should_cache bit, we leave it in the cache for now and schedule
|
||||
# its removal later.
|
||||
@@ -254,10 +260,29 @@ class ResponseCache(Generic[KV]):
|
||||
self.unset(key)
|
||||
return r
|
||||
|
||||
# make sure we do this *after* adding the entry to result_cache,
|
||||
# in case the result is already complete (in which case flipping the order would
|
||||
# leave us with a stuck entry in the cache).
|
||||
result.addBoth(on_complete)
|
||||
def on_fail(failure: Failure) -> None:
|
||||
"""
|
||||
If the deferred fails, unset the cache entry.
|
||||
"""
|
||||
self.unset(key)
|
||||
|
||||
# Consider the Failure handled so they don't get thrown by the reactor
|
||||
return None
|
||||
|
||||
# Two ordering constraints to be aware of for registering the callback and errback:
|
||||
# 1. Both of them must be registered _after_ adding the entry to the result_cache,
|
||||
# otherwise it is possible for them to trigger immediately (before the entry
|
||||
# is added to the result_cache), the net effect of which is that it would leave
|
||||
# us with a stuck entry in the cache.
|
||||
# 2. We must register the errback after callback.
|
||||
# If the errback was registered first, `on_fail` returning `None` would
|
||||
# cause `on_succeed` to be called with that `None` as argument.
|
||||
# This would start a timer to remove a cache entry, even though there isn't a
|
||||
# valid cache entry yet.
|
||||
# (This could prematurely remove a future cache entry with the same key.)
|
||||
result.addCallback(on_succeed)
|
||||
result.addErrback(on_fail)
|
||||
|
||||
return entry
|
||||
|
||||
def unset(self, key: KV) -> None:
|
||||
|
||||
@@ -1,21 +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:
|
||||
# <https://www.gnu.org/licenses/agpl-3.0.html>.
|
||||
#
|
||||
|
||||
import enum
|
||||
|
||||
|
||||
class Sentinel(enum.Enum):
|
||||
# defining a sentinel in this way allows mypy to correctly handle the
|
||||
# type of a dictionary lookup and subsequent type narrowing.
|
||||
UNSET_SENTINEL = object()
|
||||
@@ -103,6 +103,7 @@ async def filter_and_transform_events_for_client(
|
||||
Returns:
|
||||
The filtered events, wrapped in FilteredEvent with the requesting user's
|
||||
membership at each event annotated for use during serialization (MSC4115).
|
||||
The events are returned in the same order.
|
||||
"""
|
||||
# Filter out events that have been soft failed so that we don't relay them
|
||||
# to clients, unless they're a server admin and want that to happen.
|
||||
|
||||
@@ -60,7 +60,7 @@ class MSC3861OAuthDelegation(TestCase):
|
||||
|
||||
def setUp(self) -> None:
|
||||
self.config_dict: JsonDict = {
|
||||
**default_config("test"),
|
||||
**default_config(server_name="test"),
|
||||
"public_baseurl": BASE_URL,
|
||||
"enable_registration": False,
|
||||
"experimental_features": {
|
||||
@@ -287,7 +287,7 @@ class MasAuthDelegation(TestCase):
|
||||
|
||||
def setUp(self) -> None:
|
||||
self.config_dict: JsonDict = {
|
||||
**default_config("test"),
|
||||
**default_config(server_name="test"),
|
||||
"public_baseurl": BASE_URL,
|
||||
"enable_registration": False,
|
||||
"matrix_authentication_service": {
|
||||
|
||||
@@ -57,7 +57,7 @@ class ParseRatelimitSettingsTestcase(TestCase):
|
||||
|
||||
class RatelimitConfigTestCase(TestCase):
|
||||
def test_parse_rc_federation(self) -> None:
|
||||
config_dict = default_config("test")
|
||||
config_dict = default_config(server_name="test")
|
||||
config_dict["rc_federation"] = {
|
||||
"window_size": 20000,
|
||||
"sleep_limit": 693,
|
||||
|
||||
@@ -37,7 +37,7 @@ class RegistrationConfigTestCase(ConfigFileTestCase):
|
||||
Test that the user is faced with configuration errors if they make it
|
||||
smaller, as that configuration doesn't make sense.
|
||||
"""
|
||||
config_dict = default_config("test")
|
||||
config_dict = default_config(server_name="test")
|
||||
|
||||
# First test all the error conditions
|
||||
with self.assertRaises(ConfigError):
|
||||
|
||||
@@ -31,7 +31,7 @@ class RoomListHandlerTestCase(unittest.HomeserverTestCase):
|
||||
return room_id
|
||||
|
||||
def default_config(self) -> JsonDict:
|
||||
config = default_config("test")
|
||||
config = default_config(server_name="test")
|
||||
config["room_list_publication_rules"] = [{"action": "allow"}]
|
||||
return config
|
||||
|
||||
|
||||
@@ -1219,6 +1219,60 @@ class RoomSummaryTestCase(unittest.HomeserverTestCase):
|
||||
result = self.get_success(self.handler.get_room_summary(user2, self.room))
|
||||
self.assertEqual(result.get("room_id"), self.room)
|
||||
|
||||
def test_allowed_room_ids_local(self) -> None:
|
||||
"""allowed_room_ids is returned for a local room with restricted join rules."""
|
||||
# Create a space that the restricted room will allow membership from.
|
||||
space = self.helper.create_room_as(
|
||||
self.user,
|
||||
tok=self.token,
|
||||
extra_content={
|
||||
"creation_content": {"type": RoomTypes.SPACE},
|
||||
"initial_state": [
|
||||
{
|
||||
"type": EventTypes.JoinRules,
|
||||
"state_key": "",
|
||||
"content": {"join_rule": JoinRules.PUBLIC},
|
||||
}
|
||||
],
|
||||
},
|
||||
)
|
||||
|
||||
# Create a room version 8 room with join_rule=restricted allowing members
|
||||
# of the space above.
|
||||
restricted_room = self.helper.create_room_as(
|
||||
self.user,
|
||||
room_version=RoomVersions.V8.identifier,
|
||||
tok=self.token,
|
||||
extra_content={
|
||||
"initial_state": [
|
||||
{
|
||||
"type": EventTypes.JoinRules,
|
||||
"state_key": "",
|
||||
"content": {
|
||||
"join_rule": JoinRules.RESTRICTED,
|
||||
"allow": [
|
||||
{
|
||||
"type": RestrictedJoinRuleTypes.ROOM_MEMBERSHIP,
|
||||
"room_id": space,
|
||||
}
|
||||
],
|
||||
},
|
||||
}
|
||||
]
|
||||
},
|
||||
)
|
||||
|
||||
result = self.get_success(
|
||||
self.handler.get_room_summary(self.user, restricted_room)
|
||||
)
|
||||
self.assertEqual(result.get("room_id"), restricted_room)
|
||||
self.assertEqual(result.get("allowed_room_ids"), [space])
|
||||
|
||||
def test_allowed_room_ids_absent_without_restricted_join_rules(self) -> None:
|
||||
"""allowed_room_ids is absent for rooms that do not use restricted join rules."""
|
||||
result = self.get_success(self.handler.get_room_summary(self.user, self.room))
|
||||
self.assertNotIn("allowed_room_ids", result)
|
||||
|
||||
def test_fed(self) -> None:
|
||||
"""
|
||||
Return data over federation and ensure that it is handled properly.
|
||||
|
||||
@@ -77,7 +77,7 @@ class MatrixFederationAgentTests(unittest.TestCase):
|
||||
|
||||
self.mock_resolver = AsyncMock(spec=SrvResolver)
|
||||
|
||||
config_dict = default_config("test", parse=False)
|
||||
config_dict = default_config(server_name="test", parse=False)
|
||||
config_dict["federation_custom_ca_list"] = [get_test_ca_cert_file()]
|
||||
|
||||
self._config = config = HomeServerConfig()
|
||||
@@ -1019,7 +1019,7 @@ class MatrixFederationAgentTests(unittest.TestCase):
|
||||
self.mock_resolver.resolve_service.return_value = []
|
||||
self.reactor.lookups["testserv"] = "1.2.3.4"
|
||||
|
||||
config = default_config("test", parse=True)
|
||||
config = default_config(server_name="test", parse=True)
|
||||
|
||||
# Build a new agent and WellKnownResolver with a different tls factory
|
||||
tls_factory = FederationPolicyForHTTPS(config)
|
||||
|
||||
@@ -930,7 +930,7 @@ class SpamCheckerTestCaseLegacy(unittest.HomeserverTestCase):
|
||||
return resources
|
||||
|
||||
def default_config(self) -> dict[str, Any]:
|
||||
config = default_config("test")
|
||||
config = default_config(server_name="test")
|
||||
|
||||
config.update(
|
||||
{
|
||||
|
||||
@@ -0,0 +1,676 @@
|
||||
#
|
||||
# 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:
|
||||
# <https://www.gnu.org/licenses/agpl_3.0.html>.
|
||||
#
|
||||
import logging
|
||||
import sqlite3
|
||||
|
||||
from twisted.internet.testing import MemoryReactor
|
||||
|
||||
import synapse.rest.admin
|
||||
import synapse.rest.client.account_data
|
||||
from synapse.api.constants import EventTypes, EventUnsignedContentFields
|
||||
from synapse.rest.client import account_data, login, register, room, sync
|
||||
from synapse.server import HomeServer
|
||||
from synapse.types import JsonDict, StreamKeyType
|
||||
from synapse.util.clock import Clock
|
||||
from synapse.util.duration import Duration
|
||||
|
||||
from tests.rest.client.sliding_sync.test_sliding_sync import SlidingSyncBase
|
||||
from tests.server import TimedOutException
|
||||
from tests.utils import USE_POSTGRES_FOR_TESTS
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
DUMMY_LISTS = {
|
||||
"main": {
|
||||
# Don't include any rooms in the top-N window
|
||||
"ranges": [[0, 0]],
|
||||
"required_state": [],
|
||||
"timeline_limit": 0,
|
||||
}
|
||||
}
|
||||
"""
|
||||
Subscription lists that can be used in the Sliding Sync request `lists` field,
|
||||
which sets up a subscription that is interested in all rooms but does not let any rooms into the window,
|
||||
thus does not return any timelines.
|
||||
|
||||
Sufficient to get sticky event updates as per MSC4354:
|
||||
|
||||
> The server MUST include sticky events across all rooms that would be matched by at least one subscription list
|
||||
> (i.e. all rooms that the client is interested in), even if the room does not appear in top-N window for that
|
||||
> subscription list at this time.
|
||||
> Rooms that would not be matched by a list are not included, as this means the client is not interested
|
||||
> in those rooms.
|
||||
>
|
||||
> — https://github.com/matrix-org/matrix-spec-proposals/pull/4354/changes#diff-d76bc1a1d612c6da37d024f5b57f7b8352939b8db8a7ee9c6b71c1a848359afdR213-R217
|
||||
"""
|
||||
|
||||
|
||||
class SlidingSyncStickyEventsExtensionTestCase(SlidingSyncBase):
|
||||
"""Tests for the sticky events sliding sync extension"""
|
||||
|
||||
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 = [
|
||||
synapse.rest.admin.register_servlets,
|
||||
login.register_servlets,
|
||||
register.register_servlets,
|
||||
room.register_servlets,
|
||||
sync.register_servlets,
|
||||
account_data.register_servlets,
|
||||
]
|
||||
|
||||
def default_config(self) -> JsonDict:
|
||||
config = super().default_config()
|
||||
# Enable sliding sync and sticky events MSCs
|
||||
config["experimental_features"] = {
|
||||
"msc3575_enabled": True,
|
||||
"msc4354_enabled": True,
|
||||
}
|
||||
return config
|
||||
|
||||
def prepare(self, reactor: MemoryReactor, clock: Clock, hs: HomeServer) -> None:
|
||||
self.store = hs.get_datastores().main
|
||||
super().prepare(reactor, clock, hs)
|
||||
|
||||
def _assert_sticky_events_response(
|
||||
self,
|
||||
response_body: JsonDict,
|
||||
expected_events_by_room: dict[str, list[str]] | None,
|
||||
) -> str | None:
|
||||
"""Assert the sliding sync response was successful and has the expected
|
||||
sticky events.
|
||||
|
||||
Args:
|
||||
response_body: Sliding Sync response body
|
||||
expected_events_by_room:
|
||||
map of room ID to list of event IDs to expect (in the order we expect them),
|
||||
or None if we expect an empty sticky events extension response
|
||||
|
||||
Returns the next_batch token from the sticky events section,
|
||||
unless we're expecting an empty response.
|
||||
"""
|
||||
extensions = response_body["extensions"]
|
||||
sticky_events = extensions.get("org.matrix.msc4354.sticky_events")
|
||||
|
||||
# If there are no expected events, we shouldn't get anything in the response
|
||||
if expected_events_by_room is None:
|
||||
self.assertIsNone(sticky_events)
|
||||
return None
|
||||
|
||||
self.assertIsNotNone(sticky_events)
|
||||
self.assertIsInstance(sticky_events["next_batch"], str)
|
||||
|
||||
actual_rooms = sticky_events["rooms"]
|
||||
# Check that we have the expected rooms
|
||||
self.assertIncludes(
|
||||
set(actual_rooms.keys()), set(expected_events_by_room.keys()), exact=True
|
||||
)
|
||||
|
||||
# Check the events in each room
|
||||
for room_id, expected_events in expected_events_by_room.items():
|
||||
actual_events = actual_rooms[room_id]["events"]
|
||||
actual_event_ids = [e["event_id"] for e in actual_events]
|
||||
self.assertEqual(actual_event_ids, expected_events)
|
||||
for actual_event in actual_events:
|
||||
# Check the sticky TTL is sent
|
||||
self.assertIn("unsigned", actual_event)
|
||||
ttl = actual_event["unsigned"][EventUnsignedContentFields.STICKY_TTL]
|
||||
self.assertIsInstance(ttl, int)
|
||||
|
||||
self.assertIn("next_batch", sticky_events)
|
||||
return sticky_events["next_batch"]
|
||||
|
||||
def test_empty_sync(self) -> None:
|
||||
"""Test that enabling sticky events extension works on initial and incremental sync,
|
||||
even if there is no data.
|
||||
"""
|
||||
user1_id = self.register_user("user1", "pass")
|
||||
user1_tok = self.login(user1_id, "pass")
|
||||
|
||||
sync_body = {
|
||||
"lists": DUMMY_LISTS,
|
||||
"extensions": {
|
||||
"org.matrix.msc4354.sticky_events": {
|
||||
"enabled": True,
|
||||
}
|
||||
},
|
||||
}
|
||||
response_body, _ = self.do_sync(sync_body, tok=user1_tok)
|
||||
# No sticky events in initial sync.
|
||||
self._assert_sticky_events_response(response_body, None)
|
||||
|
||||
# Incremental sync should also have no sticky events
|
||||
response_body, _ = self.do_sync(
|
||||
sync_body, since=response_body["pos"], tok=user1_tok
|
||||
)
|
||||
self._assert_sticky_events_response(response_body, None)
|
||||
|
||||
def test_initial_sync(self) -> None:
|
||||
"""Test that we get sticky events when we don't specify a since token
|
||||
(initial sync).
|
||||
"""
|
||||
user1_id = self.register_user("user1", "pass")
|
||||
user1_tok = self.login(user1_id, "pass")
|
||||
user2_id = self.register_user("u2", "pass")
|
||||
user2_tok = self.login(user2_id, "pass")
|
||||
|
||||
# Create a room and join both users
|
||||
room_id = self.helper.create_room_as(user2_id, tok=user2_tok)
|
||||
self.helper.join(room_id, user1_id, tok=user1_tok)
|
||||
|
||||
# Send a sticky event from user2
|
||||
sticky_event_id: str = self.helper.send_sticky_event(
|
||||
room_id,
|
||||
EventTypes.Message,
|
||||
duration=Duration(minutes=5),
|
||||
content={"body": "sticky message", "msgtype": "m.text"},
|
||||
tok=user2_tok,
|
||||
)["event_id"]
|
||||
|
||||
# Initial sync should return the sticky event
|
||||
sync_body: JsonDict = {
|
||||
"lists": DUMMY_LISTS,
|
||||
"extensions": {
|
||||
"org.matrix.msc4354.sticky_events": {
|
||||
"enabled": True,
|
||||
}
|
||||
},
|
||||
}
|
||||
response_body, _ = self.do_sync(sync_body, tok=user1_tok)
|
||||
|
||||
# Assert the response and then get the next_batch for the next sliding sync request
|
||||
next_batch = self._assert_sticky_events_response(
|
||||
response_body, {room_id: [sticky_event_id]}
|
||||
)
|
||||
assert next_batch is not None
|
||||
|
||||
# Do an incremental sync immediately again
|
||||
sync_body = {
|
||||
"lists": DUMMY_LISTS,
|
||||
"extensions": {
|
||||
"org.matrix.msc4354.sticky_events": {
|
||||
"enabled": True,
|
||||
"since": next_batch,
|
||||
}
|
||||
},
|
||||
}
|
||||
response_body, _ = self.do_sync(sync_body, tok=user1_tok)
|
||||
|
||||
# Check we don't get that event again
|
||||
self._assert_sticky_events_response(response_body, None)
|
||||
|
||||
# Send another sticky event
|
||||
sticky_event_id2: str = self.helper.send_sticky_event(
|
||||
room_id,
|
||||
EventTypes.Message,
|
||||
duration=Duration(minutes=5),
|
||||
content={"body": "another sticky message", "msgtype": "m.text"},
|
||||
tok=user1_tok,
|
||||
)["event_id"]
|
||||
|
||||
# Now the incremental sync should give us that event
|
||||
response_body, _ = self.do_sync(sync_body, tok=user1_tok)
|
||||
self._assert_sticky_events_response(
|
||||
response_body, {room_id: [sticky_event_id2]}
|
||||
)
|
||||
|
||||
def test_expired_events_not_returned(self) -> None:
|
||||
"""Test that expired sticky events are not returned."""
|
||||
user1_id = self.register_user("user1", "pass")
|
||||
user1_tok = self.login(user1_id, "pass")
|
||||
user2_id = self.register_user("u2", "pass")
|
||||
user2_tok = self.login(user2_id, "pass")
|
||||
|
||||
# Create a room
|
||||
room_id = self.helper.create_room_as(user2_id, tok=user2_tok)
|
||||
self.helper.join(room_id, user1_id, tok=user1_tok)
|
||||
|
||||
# Send a sticky event with a short duration
|
||||
sticky_event_id = self.helper.send_sticky_event(
|
||||
room_id,
|
||||
EventTypes.Message,
|
||||
duration=Duration(seconds=2),
|
||||
content={"body": "sticky message", "msgtype": "m.text"},
|
||||
tok=user2_tok,
|
||||
)["event_id"]
|
||||
|
||||
# Initial sync should return the sticky event
|
||||
sync_body = {
|
||||
"lists": DUMMY_LISTS,
|
||||
"extensions": {
|
||||
"org.matrix.msc4354.sticky_events": {
|
||||
"enabled": True,
|
||||
}
|
||||
},
|
||||
}
|
||||
response_body, _ = self.do_sync(sync_body, tok=user1_tok)
|
||||
|
||||
# We should still get the event for now
|
||||
self._assert_sticky_events_response(response_body, {room_id: [sticky_event_id]})
|
||||
|
||||
# Advance time past the sticky duration
|
||||
self.reactor.advance(3)
|
||||
|
||||
# A second initial sync should not return the expired sticky event
|
||||
response_body, _ = self.do_sync(sync_body, tok=user1_tok)
|
||||
self._assert_sticky_events_response(response_body, None)
|
||||
|
||||
def test_wait_for_new_data(self) -> None:
|
||||
"""Test that the sliding sync request waits for new sticky events to arrive.
|
||||
(Only applies to incremental syncs with a `timeout` specified).
|
||||
"""
|
||||
user1_id = self.register_user("user1", "pass")
|
||||
user1_tok = self.login(user1_id, "pass")
|
||||
user2_id = self.register_user("u2", "pass")
|
||||
user2_tok = self.login(user2_id, "pass")
|
||||
|
||||
# Create a room
|
||||
room_id = self.helper.create_room_as(user2_id, tok=user2_tok)
|
||||
self.helper.join(room_id, user1_id, tok=user1_tok)
|
||||
|
||||
# Initial sync with no sticky events
|
||||
sync_body = {
|
||||
"lists": DUMMY_LISTS,
|
||||
"extensions": {
|
||||
"org.matrix.msc4354.sticky_events": {
|
||||
"enabled": True,
|
||||
}
|
||||
},
|
||||
}
|
||||
_, from_token = self.do_sync(sync_body, tok=user1_tok)
|
||||
|
||||
# Make the sliding sync request with a timeout
|
||||
channel = self.make_request(
|
||||
"POST",
|
||||
self.sync_endpoint + "?timeout=10000" + f"&pos={from_token}",
|
||||
content=sync_body,
|
||||
access_token=user1_tok,
|
||||
await_result=False,
|
||||
)
|
||||
|
||||
# Block for 5 seconds to make sure we are in `notifier.wait_for_events(...)`
|
||||
with self.assertRaises(TimedOutException):
|
||||
channel.await_result(timeout_ms=5000)
|
||||
|
||||
# Send a sticky event to trigger new results
|
||||
sticky_event_id = self.helper.send_sticky_event(
|
||||
room_id,
|
||||
EventTypes.Message,
|
||||
duration=Duration(minutes=5),
|
||||
content={"body": "sticky message", "msgtype": "m.text"},
|
||||
tok=user2_tok,
|
||||
)["event_id"]
|
||||
|
||||
# Should respond before the 10 second timeout
|
||||
channel.await_result(timeout_ms=100)
|
||||
self.assertEqual(channel.code, 200, channel.json_body)
|
||||
|
||||
self._assert_sticky_events_response(
|
||||
channel.json_body,
|
||||
{room_id: [sticky_event_id]},
|
||||
)
|
||||
|
||||
def test_wait_for_new_data_timeout(self) -> None:
|
||||
"""
|
||||
Test that the sliding sync request waits for new sticky events to arrive
|
||||
and times out when no data arrives before the deadline.
|
||||
(Only applies to incremental syncs with a `timeout` specified).
|
||||
"""
|
||||
user1_id = self.register_user("user1", "pass")
|
||||
user1_tok = self.login(user1_id, "pass")
|
||||
user2_id = self.register_user("u2", "pass")
|
||||
user2_tok = self.login(user2_id, "pass")
|
||||
|
||||
# Create a room
|
||||
room_id = self.helper.create_room_as(user2_id, tok=user2_tok)
|
||||
self.helper.join(room_id, user1_id, tok=user1_tok)
|
||||
|
||||
# Initial sync with no sticky events
|
||||
sync_body = {
|
||||
"lists": DUMMY_LISTS,
|
||||
"extensions": {
|
||||
"org.matrix.msc4354.sticky_events": {
|
||||
"enabled": True,
|
||||
}
|
||||
},
|
||||
}
|
||||
_, from_token = self.do_sync(sync_body, tok=user1_tok)
|
||||
|
||||
# Make the sliding sync request with a timeout
|
||||
channel = self.make_request(
|
||||
"POST",
|
||||
self.sync_endpoint + "?timeout=10000" + f"&pos={from_token}",
|
||||
content=sync_body,
|
||||
access_token=user1_tok,
|
||||
await_result=False,
|
||||
)
|
||||
|
||||
# Block for 5 seconds to make sure we are `notifier.wait_for_events(...)`
|
||||
with self.assertRaises(TimedOutException):
|
||||
channel.await_result(timeout_ms=5000)
|
||||
# Wake-up `notifier.wait_for_events(...)` that will cause us test
|
||||
# `SlidingSyncResult.__bool__` for new results.
|
||||
self._bump_notifier_wait_for_events(
|
||||
# wake key is intentionally unrelated to sticky events
|
||||
user1_id,
|
||||
wake_stream_key=StreamKeyType.ACCOUNT_DATA,
|
||||
)
|
||||
# Block for a little bit more to ensure we don't see any new results.
|
||||
with self.assertRaises(TimedOutException):
|
||||
channel.await_result(timeout_ms=4000)
|
||||
# Wait for the sync to complete (wait for the rest of the 10 second timeout,
|
||||
# 5000 + 4000 + 1200 > 10000)
|
||||
channel.await_result(timeout_ms=1200)
|
||||
self.assertEqual(channel.code, 200, channel.json_body)
|
||||
|
||||
self._assert_sticky_events_response(
|
||||
channel.json_body,
|
||||
None,
|
||||
)
|
||||
|
||||
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
|
||||
"""
|
||||
user1_id = self.register_user("user1", "pass")
|
||||
user1_tok = self.login(user1_id, "pass")
|
||||
user2_id = self.register_user("user2", "pass")
|
||||
user2_tok = self.login(user2_id, "pass")
|
||||
|
||||
# Create a room
|
||||
room_id = self.helper.create_room_as(user2_id, tok=user2_tok)
|
||||
self.helper.join(room_id, user1_id, tok=user1_tok)
|
||||
|
||||
# User1 ignores user2
|
||||
channel = self.make_request(
|
||||
"PUT",
|
||||
f"/_matrix/client/v3/user/{user1_id}/account_data/m.ignored_user_list",
|
||||
{"ignored_users": {user2_id: {}}},
|
||||
access_token=user1_tok,
|
||||
)
|
||||
self.assertEqual(channel.code, 200, channel.result)
|
||||
|
||||
# User2 sends a sticky event
|
||||
sticky_event_id = self.helper.send_sticky_event(
|
||||
room_id,
|
||||
EventTypes.Message,
|
||||
duration=Duration(minutes=5),
|
||||
content={"body": "sticky from ignored user", "msgtype": "m.text"},
|
||||
tok=user2_tok,
|
||||
)["event_id"]
|
||||
|
||||
# Initial sync for user1
|
||||
sync_body = {
|
||||
"lists": {
|
||||
"main": {
|
||||
"ranges": [[0, 10]],
|
||||
"required_state": [],
|
||||
# In this test we ask for 10 events of timeline.
|
||||
"timeline_limit": 10,
|
||||
}
|
||||
},
|
||||
"extensions": {
|
||||
"org.matrix.msc4354.sticky_events": {
|
||||
"enabled": True,
|
||||
}
|
||||
},
|
||||
}
|
||||
response_body, _ = self.do_sync(sync_body, tok=user1_tok)
|
||||
|
||||
# Timeline events should not include sticky event from ignored user
|
||||
timeline_events = response_body["rooms"][room_id]["timeline"]
|
||||
timeline_event_ids = [e["event_id"] for e in timeline_events]
|
||||
|
||||
self.assertNotIn(
|
||||
sticky_event_id,
|
||||
timeline_event_ids,
|
||||
"Sticky event from ignored user should not be in timeline",
|
||||
)
|
||||
|
||||
# Sticky events section should also not include the event from ignored user
|
||||
self._assert_sticky_events_response(response_body, None)
|
||||
|
||||
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.
|
||||
|
||||
> 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.
|
||||
> — https://github.com/matrix-org/matrix-spec-proposals/blob/4340903c15e9eab1bfb2f6a31cfa08fd535f7e7c/proposals/4354-sticky-events.md#proposal
|
||||
"""
|
||||
user1_id = self.register_user("user1", "pass")
|
||||
user1_tok = self.login(user1_id, "pass")
|
||||
|
||||
# Create a room with restrictive history visibility
|
||||
room_id = self.helper.create_room_as(
|
||||
user1_id,
|
||||
tok=user1_tok,
|
||||
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=user1_tok,
|
||||
)["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=user1_tok,
|
||||
)["event_id"]
|
||||
|
||||
# Register and join a second user after the sticky event was sent
|
||||
user2_id = self.register_user("user2", "pass")
|
||||
user2_tok = self.login(user2_id, "pass")
|
||||
self.helper.join(room_id, user2_id, tok=user2_tok)
|
||||
|
||||
# User2 syncs - they should see sticky event even though
|
||||
# history visibility is "joined" and they joined after it was sent
|
||||
sync_body = {
|
||||
"lists": {
|
||||
"main": {
|
||||
"ranges": [[0, 10]],
|
||||
"required_state": [],
|
||||
# In this test, we ask for 10 events of timeline.
|
||||
"timeline_limit": 10,
|
||||
}
|
||||
},
|
||||
"extensions": {
|
||||
"org.matrix.msc4354.sticky_events": {
|
||||
"enabled": True,
|
||||
}
|
||||
},
|
||||
}
|
||||
response_body, _ = self.do_sync(sync_body, tok=user2_tok)
|
||||
|
||||
# The sticky event is fully visible in its own right,
|
||||
# but AFAICT the timeline only includes events since we join the room
|
||||
# (regardless of history visibility),
|
||||
# so this comes down in the sticky extension
|
||||
self._assert_sticky_events_response(response_body, {room_id: [sticky_event_id]})
|
||||
|
||||
# Instead the sticky event is in the timeline
|
||||
timeline_events = response_body["rooms"][room_id]["timeline"]
|
||||
timeline_event_ids = [e["event_id"] for e in timeline_events]
|
||||
self.assertNotIn(
|
||||
regular_event_id,
|
||||
timeline_event_ids,
|
||||
f"Expecting to not see regular event ({regular_event_id}) before user1 joined.",
|
||||
)
|
||||
|
||||
def test_sticky_event_pagination(self) -> None:
|
||||
"""
|
||||
Test that pagination works correctly when there are many sticky events.
|
||||
Also check they are delivered in stream order.
|
||||
"""
|
||||
user1_id = self.register_user("user1", "pass")
|
||||
user1_tok = self.login(user1_id, "pass")
|
||||
user2_id = self.register_user("user2", "pass")
|
||||
user2_tok = self.login(user2_id, "pass")
|
||||
|
||||
# Create a room
|
||||
room_id = self.helper.create_room_as(user2_id, tok=user2_tok)
|
||||
self.helper.join(room_id, user1_id, tok=user1_tok)
|
||||
|
||||
# Send 4 sticky events (more than our limit of 2)
|
||||
sticky_event_ids: list[str] = []
|
||||
for i in range(4):
|
||||
event_id = self.helper.send_sticky_event(
|
||||
room_id,
|
||||
EventTypes.Message,
|
||||
duration=Duration(minutes=5),
|
||||
content={"body": f"sticky message {i}", "msgtype": "m.text"},
|
||||
tok=user2_tok,
|
||||
)["event_id"]
|
||||
sticky_event_ids.append(event_id)
|
||||
|
||||
# Initial sync
|
||||
sync_body = {
|
||||
"lists": DUMMY_LISTS,
|
||||
"extensions": {
|
||||
"org.matrix.msc4354.sticky_events": {"enabled": True, "limit": 2}
|
||||
},
|
||||
}
|
||||
response_body, _ = self.do_sync(sync_body, tok=user1_tok)
|
||||
|
||||
# We expect to see the first 2 sticky events by stream order
|
||||
# and they should be in that stream order
|
||||
next_batch = self._assert_sticky_events_response(
|
||||
response_body, {room_id: sticky_event_ids[0:2]}
|
||||
)
|
||||
|
||||
# Incremental sync to get remaining sticky events
|
||||
sync_body = {
|
||||
"lists": DUMMY_LISTS,
|
||||
"extensions": {
|
||||
"org.matrix.msc4354.sticky_events": {
|
||||
"enabled": True,
|
||||
# This makes it incremental
|
||||
"since": next_batch,
|
||||
"limit": 3,
|
||||
}
|
||||
},
|
||||
}
|
||||
response_body, _ = self.do_sync(sync_body, tok=user1_tok)
|
||||
|
||||
# Should get remaining events, in stream order again
|
||||
self._assert_sticky_events_response(
|
||||
response_body, {room_id: sticky_event_ids[2:4]}
|
||||
)
|
||||
|
||||
def test_deduplication_with_timeline(self) -> None:
|
||||
"""
|
||||
Test that sticky events are not included in the sticky event extension of sliding sync
|
||||
if they are included in the main timeline section.
|
||||
|
||||
Send 3 events:
|
||||
1. sticky
|
||||
2. sticky
|
||||
3. regular
|
||||
|
||||
We then will sync with a timeline limit of 2 and a sticky event limit of 2.
|
||||
We should then see (2) and (3) included in the timeline
|
||||
and (1) in the sticky event response (but not (2) because it's already
|
||||
included in the timeline.)
|
||||
|
||||
1. sticky [in sticky section]
|
||||
|
||||
------------->>> Timeline section
|
||||
2. sticky
|
||||
3. regular
|
||||
-------------<<<
|
||||
"""
|
||||
user1_id = self.register_user("user1", "pass")
|
||||
user1_tok = self.login(user1_id, "pass")
|
||||
room_id = self.helper.create_room_as(user1_id, tok=user1_tok)
|
||||
|
||||
sticky_event_ids: list[str] = []
|
||||
for i in range(2):
|
||||
event_id = self.helper.send_sticky_event(
|
||||
room_id,
|
||||
EventTypes.Message,
|
||||
duration=Duration(minutes=5),
|
||||
content={"body": f"sticky message {i}", "msgtype": "m.text"},
|
||||
tok=user1_tok,
|
||||
)["event_id"]
|
||||
sticky_event_ids.append(event_id)
|
||||
|
||||
non_sticky_event_id = self.helper.send_event(
|
||||
room_id,
|
||||
EventTypes.Message,
|
||||
content={"body": "regular message", "msgtype": "m.text"},
|
||||
tok=user1_tok,
|
||||
)["event_id"]
|
||||
|
||||
# Sync
|
||||
sync_body = {
|
||||
"lists": {
|
||||
"main": {
|
||||
"ranges": [[0, 10]],
|
||||
"required_state": [],
|
||||
# In this test, we want a timeline window of the 2 latest messages
|
||||
"timeline_limit": 2,
|
||||
}
|
||||
},
|
||||
"extensions": {
|
||||
"org.matrix.msc4354.sticky_events": {
|
||||
"enabled": True,
|
||||
"limit": 2,
|
||||
}
|
||||
},
|
||||
}
|
||||
response_body, _ = self.do_sync(sync_body, tok=user1_tok)
|
||||
|
||||
events_in_sticky_section = response_body["extensions"][
|
||||
"org.matrix.msc4354.sticky_events"
|
||||
]["rooms"][room_id]["events"]
|
||||
event_ids_in_sticky_section = [e["event_id"] for e in events_in_sticky_section]
|
||||
|
||||
events_in_timeline_section = response_body["rooms"][room_id]["timeline"]
|
||||
event_ids_in_timeline_section = [
|
||||
e["event_id"] for e in events_in_timeline_section
|
||||
]
|
||||
|
||||
self.assertEqual(
|
||||
event_ids_in_sticky_section,
|
||||
[sticky_event_ids[0]],
|
||||
)
|
||||
self.assertEqual(
|
||||
event_ids_in_timeline_section, [sticky_event_ids[1], non_sticky_event_id]
|
||||
)
|
||||
@@ -374,7 +374,7 @@ class SlidingSyncBase(unittest.HomeserverTestCase):
|
||||
user_id: The user ID to wake up the notifier for
|
||||
wake_stream_key: The stream key to wake up. This will create an actual new
|
||||
entity in that stream so it's best to choose one that won't affect the
|
||||
Sliding Sync results you're testing for. In other words, if your testing
|
||||
Sliding Sync results you're testing for. In other words, if you're testing
|
||||
account data, choose `StreamKeyType.PRESENCE` instead. We support two
|
||||
possible stream keys because you're probably testing one or the other so
|
||||
one is always a "safe" option.
|
||||
|
||||
@@ -317,7 +317,7 @@ class DelayedEventsTestCase(HomeserverTestCase):
|
||||
)
|
||||
def test_cancel_delayed_event_ratelimit(self, action_in_path: bool) -> None:
|
||||
delay_ids = []
|
||||
for _ in range(2):
|
||||
for _ in range(3):
|
||||
channel = self.make_request(
|
||||
"POST",
|
||||
_get_path_for_delayed_send(self.room_id, _EVENT_TYPE, 100000),
|
||||
@@ -329,12 +329,39 @@ class DelayedEventsTestCase(HomeserverTestCase):
|
||||
assert delay_id is not None
|
||||
delay_ids.append(delay_id)
|
||||
|
||||
channel = self._update_delayed_event(delay_ids.pop(0), "cancel", action_in_path)
|
||||
delay_id = delay_ids.pop(0)
|
||||
channel = self._update_delayed_event(delay_id, "cancel", action_in_path)
|
||||
self.assertEqual(HTTPStatus.OK, channel.code, channel.result)
|
||||
|
||||
channel = self._update_delayed_event(delay_ids.pop(0), "cancel", action_in_path)
|
||||
delay_id = delay_ids.pop(0)
|
||||
channel = self._update_delayed_event(delay_id, "cancel", action_in_path)
|
||||
self.assertEqual(HTTPStatus.TOO_MANY_REQUESTS, channel.code, channel.result)
|
||||
|
||||
# Using auth should bypass ratelimit applied against source IP
|
||||
channel = self._update_delayed_event(
|
||||
delay_id, "cancel", action_in_path, self.user1_access_token
|
||||
)
|
||||
self.assertEqual(HTTPStatus.OK, channel.code, channel.result)
|
||||
|
||||
delay_id = delay_ids.pop(0)
|
||||
channel = self._update_delayed_event(
|
||||
delay_id, "cancel", action_in_path, self.user1_access_token
|
||||
)
|
||||
self.assertEqual(HTTPStatus.TOO_MANY_REQUESTS, channel.code, channel.result)
|
||||
|
||||
# Add the current user to the ratelimit overrides, allowing them no ratelimiting.
|
||||
self.get_success(
|
||||
self.hs.get_datastores().main.set_ratelimit_for_user(
|
||||
self.user1_user_id, 0, 0
|
||||
)
|
||||
)
|
||||
|
||||
# Test that the request isn't ratelimited anymore.
|
||||
channel = self._update_delayed_event(
|
||||
delay_id, "cancel", action_in_path, self.user1_access_token
|
||||
)
|
||||
self.assertEqual(HTTPStatus.OK, channel.code, channel.result)
|
||||
|
||||
@parameterized.expand(
|
||||
(
|
||||
(content_property_value, action_in_path)
|
||||
@@ -475,7 +502,7 @@ class DelayedEventsTestCase(HomeserverTestCase):
|
||||
)
|
||||
def test_restart_delayed_event_ratelimit(self, action_in_path: bool) -> None:
|
||||
delay_ids = []
|
||||
for _ in range(2):
|
||||
for _ in range(3):
|
||||
channel = self.make_request(
|
||||
"POST",
|
||||
_get_path_for_delayed_send(self.room_id, _EVENT_TYPE, 100000),
|
||||
@@ -487,16 +514,39 @@ class DelayedEventsTestCase(HomeserverTestCase):
|
||||
assert delay_id is not None
|
||||
delay_ids.append(delay_id)
|
||||
|
||||
delay_id = delay_ids.pop(0)
|
||||
channel = self._update_delayed_event(delay_id, "restart", action_in_path)
|
||||
self.assertEqual(HTTPStatus.OK, channel.code, channel.result)
|
||||
|
||||
delay_id = delay_ids.pop(0)
|
||||
channel = self._update_delayed_event(delay_id, "restart", action_in_path)
|
||||
self.assertEqual(HTTPStatus.TOO_MANY_REQUESTS, channel.code, channel.result)
|
||||
|
||||
# Using auth should bypass ratelimit applied against source IP
|
||||
channel = self._update_delayed_event(
|
||||
delay_ids.pop(0), "restart", action_in_path
|
||||
delay_id, "restart", action_in_path, self.user1_access_token
|
||||
)
|
||||
self.assertEqual(HTTPStatus.OK, channel.code, channel.result)
|
||||
|
||||
delay_id = delay_ids.pop(0)
|
||||
channel = self._update_delayed_event(
|
||||
delay_ids.pop(0), "restart", action_in_path
|
||||
delay_id, "restart", action_in_path, self.user1_access_token
|
||||
)
|
||||
self.assertEqual(HTTPStatus.TOO_MANY_REQUESTS, channel.code, channel.result)
|
||||
|
||||
# Add the current user to the ratelimit overrides, allowing them no ratelimiting.
|
||||
self.get_success(
|
||||
self.hs.get_datastores().main.set_ratelimit_for_user(
|
||||
self.user1_user_id, 0, 0
|
||||
)
|
||||
)
|
||||
|
||||
# Test that the request isn't ratelimited anymore.
|
||||
channel = self._update_delayed_event(
|
||||
delay_id, "restart", action_in_path, self.user1_access_token
|
||||
)
|
||||
self.assertEqual(HTTPStatus.OK, channel.code, channel.result)
|
||||
|
||||
def test_delayed_state_is_not_cancelled_by_new_state_from_same_user(
|
||||
self,
|
||||
) -> None:
|
||||
@@ -612,7 +662,11 @@ class DelayedEventsTestCase(HomeserverTestCase):
|
||||
return content
|
||||
|
||||
def _update_delayed_event(
|
||||
self, delay_id: str, action: str, action_in_path: bool
|
||||
self,
|
||||
delay_id: str,
|
||||
action: str,
|
||||
action_in_path: bool,
|
||||
access_token: str | None = None,
|
||||
) -> FakeChannel:
|
||||
path = f"{PATH_PREFIX}/{delay_id}"
|
||||
body = {}
|
||||
@@ -620,7 +674,7 @@ class DelayedEventsTestCase(HomeserverTestCase):
|
||||
path += f"/{action}"
|
||||
else:
|
||||
body["action"] = action
|
||||
return self.make_request("POST", path, body)
|
||||
return self.make_request("POST", path, body, access_token)
|
||||
|
||||
def _find_sent_delayed_event(
|
||||
self, access_token: str, delay_id: str, should_find: bool
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
# This file is licensed under the Affero General Public License (AGPL) version 3.
|
||||
#
|
||||
# Copyright (C) 2023 New Vector, Ltd
|
||||
# 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
|
||||
@@ -25,7 +26,7 @@ from twisted.internet.testing import MemoryReactor
|
||||
from synapse.api.constants import EventTypes
|
||||
from synapse.events.utils import FilteredEvent
|
||||
from synapse.rest import admin
|
||||
from synapse.rest.client import login, room
|
||||
from synapse.rest.client import login, retention, room
|
||||
from synapse.server import HomeServer
|
||||
from synapse.types import JsonDict, create_requester
|
||||
from synapse.util.clock import Clock
|
||||
@@ -394,3 +395,122 @@ class RetentionNoDefaultPolicyTestCase(unittest.HomeserverTestCase):
|
||||
self.assertEqual(channel.code, expected_code, channel.result)
|
||||
|
||||
return channel.json_body
|
||||
|
||||
|
||||
RETENTION_CONFIGURATION_URL = (
|
||||
"/_matrix/client/unstable/org.matrix.msc1763/retention/configuration"
|
||||
)
|
||||
|
||||
|
||||
class RetentionConfigurationEndpointTestCase(unittest.HomeserverTestCase):
|
||||
servlets = [
|
||||
admin.register_servlets,
|
||||
login.register_servlets,
|
||||
retention.register_servlets,
|
||||
]
|
||||
|
||||
def prepare(self, reactor: MemoryReactor, clock: Clock, hs: HomeServer) -> None:
|
||||
self.register_user("user", "password")
|
||||
self.token = self.login("user", "password")
|
||||
|
||||
@override_config(
|
||||
{
|
||||
"experimental_features": {"msc1763_enabled": True},
|
||||
}
|
||||
)
|
||||
def test_disabled_returns_404_no_retention(self) -> None:
|
||||
"""The endpoint must 404 when retention is not enabled."""
|
||||
channel = self.make_request(
|
||||
"GET", RETENTION_CONFIGURATION_URL, access_token=self.token
|
||||
)
|
||||
self.assertEqual(channel.code, 404, channel.result)
|
||||
|
||||
@override_config(
|
||||
{
|
||||
"retention": {
|
||||
"enabled": True,
|
||||
"default_policy": {
|
||||
"min_lifetime": one_day_ms,
|
||||
"max_lifetime": one_day_ms * 3,
|
||||
},
|
||||
"allowed_lifetime_min": one_day_ms,
|
||||
"allowed_lifetime_max": one_day_ms * 3,
|
||||
},
|
||||
"experimental_features": {"msc1763_enabled": False},
|
||||
}
|
||||
)
|
||||
def test_disabled_returns_404_no_msc1763_enabled(self) -> None:
|
||||
"""The endpoint must 404 when retention is not enabled."""
|
||||
channel = self.make_request(
|
||||
"GET", RETENTION_CONFIGURATION_URL, access_token=self.token
|
||||
)
|
||||
self.assertEqual(channel.code, 404, channel.result)
|
||||
|
||||
@override_config(
|
||||
{
|
||||
"retention": {
|
||||
"enabled": True,
|
||||
"default_policy": {
|
||||
"min_lifetime": one_day_ms,
|
||||
"max_lifetime": one_day_ms * 3,
|
||||
},
|
||||
"allowed_lifetime_min": one_day_ms,
|
||||
"allowed_lifetime_max": one_day_ms * 3,
|
||||
},
|
||||
"experimental_features": {"msc1763_enabled": True},
|
||||
}
|
||||
)
|
||||
def test_full_config(self) -> None:
|
||||
"""Returns default policy and max_lifetime limits when fully configured."""
|
||||
channel = self.make_request(
|
||||
"GET", RETENTION_CONFIGURATION_URL, access_token=self.token
|
||||
)
|
||||
self.assertEqual(channel.code, 200, channel.result)
|
||||
body = channel.json_body
|
||||
self.assertEqual(
|
||||
body["policies"]["*"],
|
||||
{"min_lifetime": one_day_ms, "max_lifetime": one_day_ms * 3},
|
||||
)
|
||||
self.assertEqual(
|
||||
body["limits"]["max_lifetime"],
|
||||
{"min": one_day_ms, "max": one_day_ms * 3},
|
||||
)
|
||||
|
||||
@override_config(
|
||||
{
|
||||
"retention": {"enabled": True},
|
||||
"experimental_features": {"msc1763_enabled": True},
|
||||
}
|
||||
)
|
||||
def test_no_default_policy_no_limits(self) -> None:
|
||||
"""Returns empty policies and limits when nothing is configured."""
|
||||
channel = self.make_request(
|
||||
"GET", RETENTION_CONFIGURATION_URL, access_token=self.token
|
||||
)
|
||||
self.assertEqual(channel.code, 200, channel.result)
|
||||
body = channel.json_body
|
||||
self.assertEqual(body["policies"], {})
|
||||
self.assertEqual(body["limits"], {})
|
||||
|
||||
@override_config(
|
||||
{
|
||||
"retention": {
|
||||
"enabled": True,
|
||||
"allowed_lifetime_min": one_day_ms,
|
||||
"allowed_lifetime_max": one_day_ms * 7,
|
||||
},
|
||||
"experimental_features": {"msc1763_enabled": True},
|
||||
}
|
||||
)
|
||||
def test_limits_only(self) -> None:
|
||||
"""Returns limits but no default policy entry when only limits are set."""
|
||||
channel = self.make_request(
|
||||
"GET", RETENTION_CONFIGURATION_URL, access_token=self.token
|
||||
)
|
||||
self.assertEqual(channel.code, 200, channel.result)
|
||||
body = channel.json_body
|
||||
self.assertNotIn("*", body["policies"])
|
||||
self.assertEqual(
|
||||
body["limits"]["max_lifetime"],
|
||||
{"min": one_day_ms, "max": one_day_ms * 7},
|
||||
)
|
||||
@@ -2785,7 +2785,7 @@ class PublicRoomsRoomTypeFilterTestCase(unittest.HomeserverTestCase):
|
||||
)
|
||||
|
||||
def default_config(self) -> JsonDict:
|
||||
config = default_config("test")
|
||||
config = default_config(server_name="test")
|
||||
config["room_list_publication_rules"] = [{"action": "allow"}]
|
||||
return config
|
||||
|
||||
|
||||
Loaded 100 of 113 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user