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