diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml
index 124b17458f..d20d30c035 100644
--- a/.github/workflows/docker.yml
+++ b/.github/workflows/docker.yml
@@ -34,32 +34,24 @@ jobs:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- # TODO: consider using https://github.com/docker/metadata-action instead of this
- # custom magic
- name: Calculate docker image tag
id: set-tag
- run: |
- case "${GITHUB_REF}" in
- refs/heads/develop)
- tag=develop
- ;;
- refs/heads/master|refs/heads/main)
- tag=latest
- ;;
- refs/tags/*)
- tag=${GITHUB_REF#refs/tags/}
- ;;
- *)
- tag=${GITHUB_SHA}
- ;;
- esac
- echo "::set-output name=tag::$tag"
+ uses: docker/metadata-action@master
+ with:
+ images: matrixdotorg/synapse
+ flavor: |
+ latest=false
+ tags: |
+ type=raw,value=develop,enable=${{ github.ref == 'refs/heads/develop' }}
+ type=raw,value=latest,enable=${{ github.ref == 'refs/heads/master' }}
+ type=raw,value=latest,enable=${{ github.ref == 'refs/heads/main' }}
+ type=pep440,pattern={{raw}}
- name: Build and push all platforms
uses: docker/build-push-action@v2
with:
push: true
labels: "gitsha1=${{ github.sha }}"
- tags: "matrixdotorg/synapse:${{ steps.set-tag.outputs.tag }}"
+ tags: "${{ steps.set-tag.outputs.tags }}"
file: "docker/Dockerfile"
platforms: linux/amd64,linux/arm64
diff --git a/.github/workflows/latest_deps.yml b/.github/workflows/latest_deps.yml
index 1a61d179d9..c537a5a60f 100644
--- a/.github/workflows/latest_deps.yml
+++ b/.github/workflows/latest_deps.yml
@@ -32,12 +32,15 @@ jobs:
with:
python-version: "3.x"
poetry-version: "1.2.0b1"
+ extras: "all"
# Dump installed versions for debugging.
- run: poetry run pip list > before.txt
# Upgrade all runtime dependencies only. This is intended to mimic a fresh
# `pip install matrix-synapse[all]` as closely as possible.
- run: poetry update --no-dev
- run: poetry run pip list > after.txt && (diff -u before.txt after.txt || true)
+ - name: Remove warn_unused_ignores from mypy config
+ run: sed '/warn_unused_ignores = True/d' -i mypy.ini
- run: poetry run mypy
trial:
runs-on: ubuntu-latest
diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml
index cad4cb6d77..efa35b71df 100644
--- a/.github/workflows/tests.yml
+++ b/.github/workflows/tests.yml
@@ -20,13 +20,9 @@ jobs:
- run: scripts-dev/config-lint.sh
lint:
- # This does a vanilla `poetry install` - no extras. I'm slightly anxious
- # that we might skip some typechecks on code that uses extras. However,
- # I think the right way to fix this is to mark any extras needed for
- # typechecking as development dependencies. To detect this, we ought to
- # turn up mypy's strictness: disallow unknown imports and be accept fewer
- # uses of `Any`.
uses: "matrix-org/backend-meta/.github/workflows/python-poetry-ci.yml@v1"
+ with:
+ typechecking-extras: "all"
lint-crlf:
runs-on: ubuntu-latest
diff --git a/.github/workflows/twisted_trunk.yml b/.github/workflows/twisted_trunk.yml
index 8fc1affb77..5f0671f350 100644
--- a/.github/workflows/twisted_trunk.yml
+++ b/.github/workflows/twisted_trunk.yml
@@ -24,6 +24,8 @@ jobs:
poetry remove twisted
poetry add --extras tls git+https://github.com/twisted/twisted.git#trunk
poetry install --no-interaction --extras "all test"
+ - name: Remove warn_unused_ignores from mypy config
+ run: sed '/warn_unused_ignores = True/d' -i mypy.ini
- run: poetry run mypy
trial:
diff --git a/CHANGES.md b/CHANGES.md
index 1fbe0815de..c625e4d561 100644
--- a/CHANGES.md
+++ b/CHANGES.md
@@ -1,3 +1,126 @@
+Synapse 1.59.0rc1 (2022-05-10)
+==============================
+
+This release makes several changes that server administrators should be aware of:
+
+- Device name lookup over federation is now disabled by default. ([\#12616](https://github.com/matrix-org/synapse/issues/12616))
+- The `synapse.app.appservice` and `synapse.app.user_dir` worker application types are now deprecated. ([\#12452](https://github.com/matrix-org/synapse/issues/12452), [\#12654](https://github.com/matrix-org/synapse/issues/12654))
+
+See [the upgrade notes](https://github.com/matrix-org/synapse/blob/develop/docs/upgrade.md#upgrading-to-v1590) for more details.
+
+Additionally, this release removes the non-standard `m.login.jwt` login type from Synapse. It can be replaced with `org.matrix.login.jwt` for identical behaviour. This is only used if `jwt_config.enabled` is set to `true` in the configuration. ([\#12597](https://github.com/matrix-org/synapse/issues/12597))
+
+Features
+--------
+
+- Support [MSC3266](https://github.com/matrix-org/matrix-doc/pull/3266) room summaries over federation. ([\#11507](https://github.com/matrix-org/synapse/issues/11507))
+- Implement [changes](https://github.com/matrix-org/matrix-spec-proposals/pull/2285/commits/4a77139249c2e830aec3c7d6bd5501a514d1cc27) to [MSC2285 (hidden read receipts)](https://github.com/matrix-org/matrix-spec-proposals/pull/2285). Contributed by @SimonBrandner. ([\#12168](https://github.com/matrix-org/synapse/issues/12168), [\#12635](https://github.com/matrix-org/synapse/issues/12635), [\#12636](https://github.com/matrix-org/synapse/issues/12636), [\#12670](https://github.com/matrix-org/synapse/issues/12670))
+- Extend the [module API](https://github.com/matrix-org/synapse/blob/release-v1.59/synapse/module_api/__init__.py) to allow modules to change actions for existing push rules of local users. ([\#12406](https://github.com/matrix-org/synapse/issues/12406))
+- Add the `notify_appservices_from_worker` configuration option (superseding `notify_appservices`) to allow a generic worker to be designated as the worker to send traffic to Application Services. ([\#12452](https://github.com/matrix-org/synapse/issues/12452))
+- Add the `update_user_directory_from_worker` configuration option (superseding `update_user_directory`) to allow a generic worker to be designated as the worker to update the user directory. ([\#12654](https://github.com/matrix-org/synapse/issues/12654))
+- Add new `enable_registration_token_3pid_bypass` configuration option to allow registrations via token as an alternative to verifying a 3pid. ([\#12526](https://github.com/matrix-org/synapse/issues/12526))
+- Implement [MSC3786](https://github.com/matrix-org/matrix-spec-proposals/pull/3786): Add a default push rule to ignore `m.room.server_acl` events. ([\#12601](https://github.com/matrix-org/synapse/issues/12601))
+- Add new `mau_appservice_trial_days` configuration option to specify a different trial period for users registered via an appservice. ([\#12619](https://github.com/matrix-org/synapse/issues/12619))
+
+
+Bugfixes
+--------
+
+- Fix a bug introduced in Synapse 1.48.0 where the latest thread reply provided failed to include the proper bundled aggregations. ([\#12273](https://github.com/matrix-org/synapse/issues/12273))
+- Fix a bug introduced in Synapse 1.22.0 where attempting to send a large amount of read receipts to an application service all at once would result in duplicate content and abnormally high memory usage. Contributed by Brad & Nick @ Beeper. ([\#12544](https://github.com/matrix-org/synapse/issues/12544))
+- Fix a bug introduced in Synapse 1.57.0 which could cause `Failed to calculate hosts in room` errors to be logged for outbound federation. ([\#12570](https://github.com/matrix-org/synapse/issues/12570))
+- Fix a long-standing bug where status codes would almost always get logged as `200!`, irrespective of the actual status code, when clients disconnect before a request has finished processing. ([\#12580](https://github.com/matrix-org/synapse/issues/12580))
+- Fix race when persisting an event and deleting a room that could lead to outbound federation breaking. ([\#12594](https://github.com/matrix-org/synapse/issues/12594))
+- Fix a bug introduced in Synapse 1.53.0 where bundled aggregations for annotations/edits were incorrectly calculated. ([\#12633](https://github.com/matrix-org/synapse/issues/12633))
+- Fix a long-standing bug where rooms containing power levels with string values could not be upgraded. ([\#12657](https://github.com/matrix-org/synapse/issues/12657))
+- Prevent memory leak from reoccurring when presence is disabled. ([\#12656](https://github.com/matrix-org/synapse/issues/12656))
+
+
+Updates to the Docker image
+---------------------------
+
+- Explicitly opt-in to using [BuildKit-specific features](https://github.com/moby/buildkit/blob/master/frontend/dockerfile/docs/syntax.md) in the Dockerfile. This fixes issues with building images in some GitLab CI environments. ([\#12541](https://github.com/matrix-org/synapse/issues/12541))
+- Update the "Build docker images" GitHub Actions workflow to use `docker/metadata-action` to generate docker image tags, instead of a custom shell script. Contributed by @henryclw. ([\#12573](https://github.com/matrix-org/synapse/issues/12573))
+
+
+Improved Documentation
+----------------------
+
+- Update SQL statements and replace use of old table `user_stats_historical` in docs for Synapse Admins. ([\#12536](https://github.com/matrix-org/synapse/issues/12536))
+- Add missing linebreak to `pipx` install instructions. ([\#12579](https://github.com/matrix-org/synapse/issues/12579))
+- Add information about the TCP replication module to docs. ([\#12621](https://github.com/matrix-org/synapse/issues/12621))
+- Fixes to the formatting of `README.rst`. ([\#12627](https://github.com/matrix-org/synapse/issues/12627))
+- Fix docs on how to run specific Complement tests using the `complement.sh` test runner. ([\#12664](https://github.com/matrix-org/synapse/issues/12664))
+
+
+Deprecations and Removals
+-------------------------
+
+- Remove unstable identifiers from [MSC3069](https://github.com/matrix-org/matrix-doc/pull/3069). ([\#12596](https://github.com/matrix-org/synapse/issues/12596))
+- Remove the unspecified `m.login.jwt` login type and the unstable `uk.half-shot.msc2778.login.application_service` from
+ [MSC2778](https://github.com/matrix-org/matrix-doc/pull/2778). ([\#12597](https://github.com/matrix-org/synapse/issues/12597))
+- Synapse now requires at least Python 3.7.1 (up from 3.7.0), for compatibility with the latest Twisted trunk. ([\#12613](https://github.com/matrix-org/synapse/issues/12613))
+
+
+Internal Changes
+----------------
+
+- Use supervisord to supervise Postgres and Caddy in the Complement image to reduce restart time. ([\#12480](https://github.com/matrix-org/synapse/issues/12480))
+- Immediately retry any requests that have backed off when a server comes back online. ([\#12500](https://github.com/matrix-org/synapse/issues/12500))
+- Use `make_awaitable` instead of `defer.succeed` for return values of mocks in tests. ([\#12505](https://github.com/matrix-org/synapse/issues/12505))
+- Consistently check if an object is a `frozendict`. ([\#12564](https://github.com/matrix-org/synapse/issues/12564))
+- Protect module callbacks with read semantics against cancellation. ([\#12568](https://github.com/matrix-org/synapse/issues/12568))
+- Improve comments and error messages around access tokens. ([\#12577](https://github.com/matrix-org/synapse/issues/12577))
+- Improve docstrings for the receipts store. ([\#12581](https://github.com/matrix-org/synapse/issues/12581))
+- Use constants for read-receipts in tests. ([\#12582](https://github.com/matrix-org/synapse/issues/12582))
+- Log status code of cancelled requests as 499 and avoid logging stack traces for them. ([\#12587](https://github.com/matrix-org/synapse/issues/12587), [\#12663](https://github.com/matrix-org/synapse/issues/12663))
+- Remove special-case for `twisted` logger from default log config. ([\#12589](https://github.com/matrix-org/synapse/issues/12589))
+- Use `getClientAddress` instead of the deprecated `getClientIP`. ([\#12599](https://github.com/matrix-org/synapse/issues/12599))
+- Add link to documentation in Grafana Dashboard. ([\#12602](https://github.com/matrix-org/synapse/issues/12602))
+- Reduce log spam when running multiple event persisters. ([\#12610](https://github.com/matrix-org/synapse/issues/12610))
+- Add extra debug logging to federation sender. ([\#12614](https://github.com/matrix-org/synapse/issues/12614))
+- Prevent remote homeservers from requesting local user device names by default. ([\#12616](https://github.com/matrix-org/synapse/issues/12616))
+- Add a consistency check on events which we read from the database. ([\#12620](https://github.com/matrix-org/synapse/issues/12620))
+- Remove use of the `constantly` library and switch to enums for `EventRedactBehaviour`. Contributed by @andrewdoh. ([\#12624](https://github.com/matrix-org/synapse/issues/12624))
+- Remove unused code related to receipts. ([\#12632](https://github.com/matrix-org/synapse/issues/12632))
+- Minor improvements to the scripts for running Synapse in worker mode under Complement. ([\#12637](https://github.com/matrix-org/synapse/issues/12637))
+- Move `pympler` back in to the `all` extras. ([\#12652](https://github.com/matrix-org/synapse/issues/12652))
+- Fix spelling of `M_UNRECOGNIZED` in comments. ([\#12665](https://github.com/matrix-org/synapse/issues/12665))
+- Release script: confirm the commit to be tagged before tagging. ([\#12556](https://github.com/matrix-org/synapse/issues/12556))
+- Fix a typo in the announcement text generated by the Synapse release development script. ([\#12612](https://github.com/matrix-org/synapse/issues/12612))
+
+### Typechecking
+
+- Fix scripts-dev to pass typechecking. ([\#12356](https://github.com/matrix-org/synapse/issues/12356))
+- Add some type hints to datastore. ([\#12485](https://github.com/matrix-org/synapse/issues/12485))
+- Remove unused `# type: ignore`s. ([\#12531](https://github.com/matrix-org/synapse/issues/12531))
+- Allow unused `# type: ignore` comments in bleeding edge CI jobs. ([\#12576](https://github.com/matrix-org/synapse/issues/12576))
+- Remove redundant lines of config from `mypy.ini`. ([\#12608](https://github.com/matrix-org/synapse/issues/12608))
+- Update to mypy 0.950. ([\#12650](https://github.com/matrix-org/synapse/issues/12650))
+- Use `Concatenate` to better annotate `_do_execute`. ([\#12666](https://github.com/matrix-org/synapse/issues/12666))
+- Use `ParamSpec` to refine type hints. ([\#12667](https://github.com/matrix-org/synapse/issues/12667))
+- Fix mypy against latest pillow stubs. ([\#12671](https://github.com/matrix-org/synapse/issues/12671))
+
+Synapse 1.58.1 (2022-05-05)
+===========================
+
+This patch release includes a fix to the Debian packages, installing the
+`systemd` and `cache_memory` extra package groups, which were incorrectly
+omitted in v1.58.0. This primarily prevented Synapse from starting
+when the `systemd.journal.JournalHandler` log handler was configured.
+See [#12631](https://github.com/matrix-org/synapse/issues/12631) for further information.
+
+Otherwise, no significant changes since 1.58.0.
+
+
+Synapse 1.58.0 (2022-05-03)
+===========================
+
+As of this release, the groups/communities feature in Synapse is now disabled by default. See [\#11584](https://github.com/matrix-org/synapse/issues/11584) for details. As mentioned in [the upgrade notes](https://github.com/matrix-org/synapse/blob/develop/docs/upgrade.md#upgrading-to-v1580), this feature will be removed in Synapse 1.61.
+
+No significant changes since 1.58.0rc2.
+
+
Synapse 1.58.0rc2 (2022-04-26)
==============================
@@ -19,8 +142,6 @@ Internal Changes
Synapse 1.58.0rc1 (2022-04-26)
==============================
-As of this release, the groups/communities feature in Synapse is now disabled by default. See [\#11584](https://github.com/matrix-org/synapse/issues/11584) for details. As mentioned in [the upgrade notes](https://github.com/matrix-org/synapse/blob/develop/docs/upgrade.md#upgrading-to-v1580), this feature will be removed in Synapse 1.61.
-
Features
--------
diff --git a/README.rst b/README.rst
index d71d733679..219e32de8e 100644
--- a/README.rst
+++ b/README.rst
@@ -55,7 +55,7 @@ solutions. The hope is for Matrix to act as the building blocks for a new
generation of fully open and interoperable messaging and VoIP apps for the
internet.
-Synapse is a Matrix "homeserver" implementation developed by the matrix.org core
+Synapse is a Matrix "homeserver" implementation developed by the matrix.org core
team, written in Python 3/Twisted.
In Matrix, every user runs one or more Matrix clients, which connect through to
@@ -294,13 +294,13 @@ directory of your choice::
cd synapse
Synapse has a number of external dependencies. We maintain a fixed development
-environment using [poetry](https://python-poetry.org/). First, install poetry. We recommend
+environment using `Poetry `_. First, install poetry. We recommend::
pip install --user pipx
pipx install poetry
as described `here `_.
-(See `poetry's installation docs `
+(See `poetry's installation docs `_
for other installation methods.) Then ask poetry to create a virtual environment
from the project and install Synapse's dependencies::
@@ -309,11 +309,11 @@ from the project and install Synapse's dependencies::
This will run a process of downloading and installing all the needed
dependencies into a virtual env.
-We recommend using the demo which starts 3 federated instances running on ports `8080` - `8082`
+We recommend using the demo which starts 3 federated instances running on ports `8080` - `8082`::
poetry run ./demo/start.sh
-(to stop, you can use `poetry run ./demo/stop.sh`)
+(to stop, you can use ``poetry run ./demo/stop.sh``)
See the `demo documentation `_
for more information.
diff --git a/contrib/grafana/synapse.json b/contrib/grafana/synapse.json
index 2c839c30d0..819426b8ea 100644
--- a/contrib/grafana/synapse.json
+++ b/contrib/grafana/synapse.json
@@ -66,6 +66,18 @@
],
"title": "Dashboards",
"type": "dashboards"
+ },
+ {
+ "asDropdown": false,
+ "icon": "external link",
+ "includeVars": false,
+ "keepTime": false,
+ "tags": [],
+ "targetBlank": true,
+ "title": "Synapse Documentation",
+ "tooltip": "Open Documentation",
+ "type": "link",
+ "url": "https://matrix-org.github.io/synapse/latest/"
}
],
"panels": [
@@ -10889,4 +10901,4 @@
"title": "Synapse",
"uid": "000000012",
"version": 100
-}
\ No newline at end of file
+}
diff --git a/debian/build_virtualenv b/debian/build_virtualenv
index b068792592..f1ec609163 100755
--- a/debian/build_virtualenv
+++ b/debian/build_virtualenv
@@ -37,7 +37,11 @@ python3 -m venv "$TEMP_VENV"
source "$TEMP_VENV/bin/activate"
pip install -U pip
pip install poetry==1.2.0b1
-poetry export --extras all --extras test -o exported_requirements.txt
+poetry export \
+ --extras all \
+ --extras test \
+ --extras systemd \
+ -o exported_requirements.txt
deactivate
rm -rf "$TEMP_VENV"
diff --git a/debian/changelog b/debian/changelog
index 5f1bf872bb..fabc690bae 100644
--- a/debian/changelog
+++ b/debian/changelog
@@ -1,3 +1,26 @@
+matrix-synapse-py3 (1.59.0~rc1) stable; urgency=medium
+
+ * Adjust how the `exported-requirements.txt` file is generated as part of
+ the process of building these packages. This affects the package
+ maintainers only; end-users are unaffected.
+ * New Synapse release 1.59.0rc1.
+
+ -- Synapse Packaging team Tue, 10 May 2022 10:45:08 +0100
+
+matrix-synapse-py3 (1.58.1) stable; urgency=medium
+
+ * Include python dependencies from the `systemd` and `cache_memory` extras package groups, which
+ were incorrectly omitted from the 1.58.0 package.
+ * New Synapse release 1.58.1.
+
+ -- Synapse Packaging team Thu, 05 May 2022 14:58:23 +0100
+
+matrix-synapse-py3 (1.58.0) stable; urgency=medium
+
+ * New Synapse release 1.58.0.
+
+ -- Synapse Packaging team Tue, 03 May 2022 10:52:58 +0100
+
matrix-synapse-py3 (1.58.0~rc2) stable; urgency=medium
* New Synapse release 1.58.0rc2.
diff --git a/docker/Dockerfile b/docker/Dockerfile
index 4523c60645..ccc6a9f778 100644
--- a/docker/Dockerfile
+++ b/docker/Dockerfile
@@ -1,3 +1,4 @@
+# syntax=docker/dockerfile:1
# Dockerfile to build the matrixdotorg/synapse docker images.
#
# Note that it uses features which are only available in BuildKit - see
diff --git a/docker/Dockerfile-workers b/docker/Dockerfile-workers
index 9ccb2b22a7..24b03585f9 100644
--- a/docker/Dockerfile-workers
+++ b/docker/Dockerfile-workers
@@ -20,6 +20,9 @@ RUN rm /etc/nginx/sites-enabled/default
# Copy Synapse worker, nginx and supervisord configuration template files
COPY ./docker/conf-workers/* /conf/
+# Copy a script to prefix log lines with the supervisor program name
+COPY ./docker/prefix-log /usr/local/bin/
+
# Expose nginx listener port
EXPOSE 8080/tcp
diff --git a/docker/complement/SynapseWorkers.Dockerfile b/docker/complement/SynapseWorkers.Dockerfile
index 65df2d114d..9a4438e730 100644
--- a/docker/complement/SynapseWorkers.Dockerfile
+++ b/docker/complement/SynapseWorkers.Dockerfile
@@ -34,13 +34,16 @@ WORKDIR /data
# Copy the caddy config
COPY conf-workers/caddy.complement.json /root/caddy.json
+COPY conf-workers/postgres.supervisord.conf /etc/supervisor/conf.d/postgres.conf
+COPY conf-workers/caddy.supervisord.conf /etc/supervisor/conf.d/caddy.conf
+
# Copy the entrypoint
COPY conf-workers/start-complement-synapse-workers.sh /
# Expose caddy's listener ports
EXPOSE 8008 8448
-ENTRYPOINT /start-complement-synapse-workers.sh
+ENTRYPOINT ["/start-complement-synapse-workers.sh"]
# Update the healthcheck to have a shorter check interval
HEALTHCHECK --start-period=5s --interval=1s --timeout=1s \
diff --git a/docker/complement/conf-workers/caddy.supervisord.conf b/docker/complement/conf-workers/caddy.supervisord.conf
new file mode 100644
index 0000000000..d9ddb51dac
--- /dev/null
+++ b/docker/complement/conf-workers/caddy.supervisord.conf
@@ -0,0 +1,7 @@
+[program:caddy]
+command=/usr/local/bin/prefix-log /root/caddy run --config /root/caddy.json
+autorestart=unexpected
+stdout_logfile=/dev/stdout
+stdout_logfile_maxbytes=0
+stderr_logfile=/dev/stderr
+stderr_logfile_maxbytes=0
diff --git a/docker/complement/conf-workers/postgres.supervisord.conf b/docker/complement/conf-workers/postgres.supervisord.conf
new file mode 100644
index 0000000000..5608342d1a
--- /dev/null
+++ b/docker/complement/conf-workers/postgres.supervisord.conf
@@ -0,0 +1,16 @@
+[program:postgres]
+command=/usr/local/bin/prefix-log /usr/bin/pg_ctlcluster 13 main start --foreground
+
+# Lower priority number = starts first
+priority=1
+
+autorestart=unexpected
+stdout_logfile=/dev/stdout
+stdout_logfile_maxbytes=0
+stderr_logfile=/dev/stderr
+stderr_logfile_maxbytes=0
+
+# Use 'Fast Shutdown' mode which aborts current transactions and closes connections quickly.
+# (Default (TERM) is 'Smart Shutdown' which stops accepting new connections but
+# lets existing connections close gracefully.)
+stopsignal=INT
diff --git a/docker/complement/conf-workers/start-complement-synapse-workers.sh b/docker/complement/conf-workers/start-complement-synapse-workers.sh
index 2c1e05bd62..b9a6b55bbe 100755
--- a/docker/complement/conf-workers/start-complement-synapse-workers.sh
+++ b/docker/complement/conf-workers/start-complement-synapse-workers.sh
@@ -12,12 +12,6 @@ function log {
# Replace the server name in the caddy config
sed -i "s/{{ server_name }}/${SERVER_NAME}/g" /root/caddy.json
-log "starting postgres"
-pg_ctlcluster 13 main start
-
-log "starting caddy"
-/root/caddy start --config /root/caddy.json
-
# Set the server name of the homeserver
export SYNAPSE_SERVER_NAME=${SERVER_NAME}
diff --git a/docker/conf-workers/supervisord.conf.j2 b/docker/conf-workers/supervisord.conf.j2
index 408ef72787..ca1f7aef8e 100644
--- a/docker/conf-workers/supervisord.conf.j2
+++ b/docker/conf-workers/supervisord.conf.j2
@@ -9,7 +9,7 @@ user=root
files = /etc/supervisor/conf.d/*.conf
[program:nginx]
-command=/usr/sbin/nginx -g "daemon off;"
+command=/usr/local/bin/prefix-log /usr/sbin/nginx -g "daemon off;"
priority=500
stdout_logfile=/dev/stdout
stdout_logfile_maxbytes=0
@@ -19,7 +19,7 @@ username=www-data
autorestart=true
[program:redis]
-command=/usr/bin/redis-server /etc/redis/redis.conf --daemonize no
+command=/usr/local/bin/prefix-log /usr/bin/redis-server /etc/redis/redis.conf --daemonize no
priority=1
stdout_logfile=/dev/stdout
stdout_logfile_maxbytes=0
@@ -29,7 +29,7 @@ username=redis
autorestart=true
[program:synapse_main]
-command=/usr/local/bin/python -m synapse.app.homeserver --config-path="{{ main_config_path }}" --config-path=/conf/workers/shared.yaml
+command=/usr/local/bin/prefix-log /usr/local/bin/python -m synapse.app.homeserver --config-path="{{ main_config_path }}" --config-path=/conf/workers/shared.yaml
priority=10
# Log startup failures to supervisord's stdout/err
# Regular synapse logs will still go in the configured data directory
diff --git a/docker/conf/log.config b/docker/conf/log.config
index 7a216a36a0..dc8c70befd 100644
--- a/docker/conf/log.config
+++ b/docker/conf/log.config
@@ -2,11 +2,7 @@ version: 1
formatters:
precise:
-{% if worker_name %}
- format: '%(asctime)s - worker:{{ worker_name }} - %(name)s - %(lineno)d - %(levelname)s - %(request)s - %(message)s'
-{% else %}
format: '%(asctime)s - %(name)s - %(lineno)d - %(levelname)s - %(request)s - %(message)s'
-{% endif %}
handlers:
{% if LOG_FILE_PATH %}
diff --git a/docker/configure_workers_and_start.py b/docker/configure_workers_and_start.py
index 3bda6c300b..b2b7938ae8 100755
--- a/docker/configure_workers_and_start.py
+++ b/docker/configure_workers_and_start.py
@@ -69,10 +69,10 @@ WORKERS_CONFIG: Dict[str, Dict[str, Any]] = {
"worker_extra_conf": "enable_media_repo: true",
},
"appservice": {
- "app": "synapse.app.appservice",
+ "app": "synapse.app.generic_worker",
"listener_resources": [],
"endpoint_patterns": [],
- "shared_extra_conf": {"notify_appservices": False},
+ "shared_extra_conf": {"notify_appservices_from_worker": "appservice"},
"worker_extra_conf": "",
},
"federation_sender": {
@@ -171,7 +171,7 @@ WORKERS_CONFIG: Dict[str, Dict[str, Any]] = {
# Templates for sections that may be inserted multiple times in config files
SUPERVISORD_PROCESS_CONFIG_BLOCK = """
[program:synapse_{name}]
-command=/usr/local/bin/python -m {app} \
+command=/usr/local/bin/prefix-log /usr/local/bin/python -m {app} \
--config-path="{config_path}" \
--config-path=/conf/workers/shared.yaml \
--config-path=/conf/workers/{name}.yaml
diff --git a/docker/prefix-log b/docker/prefix-log
new file mode 100755
index 0000000000..0e26a4f19d
--- /dev/null
+++ b/docker/prefix-log
@@ -0,0 +1,12 @@
+#!/bin/bash
+#
+# Prefixes all lines on stdout and stderr with the process name (as determined by
+# the SUPERVISOR_PROCESS_NAME env var, which is automatically set by Supervisor).
+#
+# Usage:
+# prefix-log command [args...]
+#
+
+exec 1> >(awk '{print "'"${SUPERVISOR_PROCESS_NAME}"' | "$0}' >&1)
+exec 2> >(awk '{print "'"${SUPERVISOR_PROCESS_NAME}"' | "$0}' >&2)
+exec "$@"
diff --git a/docs/development/contributing_guide.md b/docs/development/contributing_guide.md
index 3b5c774018..d356c72bf7 100644
--- a/docs/development/contributing_guide.md
+++ b/docs/development/contributing_guide.md
@@ -270,13 +270,13 @@ COMPLEMENT_DIR=../complement ./scripts-dev/complement.sh
To run a specific test file, you can pass the test name at the end of the command. The name passed comes from the naming structure in your Complement tests. If you're unsure of the name, you can do a full run and copy it from the test output:
```sh
-COMPLEMENT_DIR=../complement ./scripts-dev/complement.sh TestBackfillingHistory
+COMPLEMENT_DIR=../complement ./scripts-dev/complement.sh -run TestImportHistoricalMessages
```
To run a specific test, you can specify the whole name structure:
```sh
-COMPLEMENT_DIR=../complement ./scripts-dev/complement.sh TestBackfillingHistory/parallel/Backfilled_historical_events_resolve_with_proper_state_in_correct_order
+COMPLEMENT_DIR=../complement ./scripts-dev/complement.sh -run TestImportHistoricalMessages/parallel/Historical_events_resolve_in_the_correct_order
```
diff --git a/docs/jwt.md b/docs/jwt.md
index 32f58cc0cb..346daf78ad 100644
--- a/docs/jwt.md
+++ b/docs/jwt.md
@@ -17,9 +17,6 @@ follows:
}
```
-Note that the login type of `m.login.jwt` is supported, but is deprecated. This
-will be removed in a future version of Synapse.
-
The `token` field should include the JSON web token with the following claims:
* A claim that encodes the local part of the user ID is required. By default,
diff --git a/docs/replication.md b/docs/replication.md
index e82df0de8a..108da9a065 100644
--- a/docs/replication.md
+++ b/docs/replication.md
@@ -35,3 +35,8 @@ See [the TCP replication documentation](tcp_replication.md).
There are read-only version of the synapse storage layer in
`synapse/replication/slave/storage` that use the response of the
replication API to invalidate their caches.
+
+### The TCP Replication Module
+Information about how the tcp replication module is structured, including how
+the classes interact, can be found in
+`synapse/replication/tcp/__init__.py`
diff --git a/docs/sample_config.yaml b/docs/sample_config.yaml
index b8d8c0dbf0..a803b8261d 100644
--- a/docs/sample_config.yaml
+++ b/docs/sample_config.yaml
@@ -407,6 +407,11 @@ manhole_settings:
# sign up in a short space of time never to return after their initial
# session.
#
+# The option `mau_appservice_trial_days` is similar to `mau_trial_days`, but
+# applies a different trial number if the user was registered by an appservice.
+# A value of 0 means no trial days are applied. Appservices not listed in this
+# dictionary use the value of `mau_trial_days` instead.
+#
# 'mau_limit_alerting' is a means of limiting client side alerting
# should the mau limit be reached. This is useful for small instances
# where the admin has 5 mau seats (say) for 5 specific people and no
@@ -417,6 +422,8 @@ manhole_settings:
#max_mau_value: 50
#mau_trial_days: 2
#mau_limit_alerting: false
+#mau_appservice_trial_days:
+# "appservice-id": 1
# If enabled, the metrics for the number of monthly active users will
# be populated, however no one will be limited. If limit_usage_by_mau
@@ -709,11 +716,11 @@ retention:
#
#allow_profile_lookup_over_federation: false
-# Uncomment to disable device display name lookup over federation. By default, the
-# Federation API allows other homeservers to obtain device display names of any user
-# on this homeserver. Defaults to 'true'.
+# Uncomment to allow device display name lookup over federation. By default, the
+# Federation API prevents other homeservers from obtaining the display names of
+# user devices on this homeserver. Defaults to 'false'.
#
-#allow_device_name_lookup_over_federation: false
+#allow_device_name_lookup_over_federation: true
## Caching ##
@@ -1323,6 +1330,12 @@ oembed:
#
#registration_requires_token: true
+# Allow users to submit a token during registration to bypass any required 3pid
+# steps configured in `registrations_require_3pid`.
+# Defaults to false, requiring that registration tokens (if enabled) complete a 3pid flow.
+#
+#enable_registration_token_3pid_bypass: false
+
# If set, allows registration of standard or admin accounts by anyone who
# has the shared secret, even if registration is otherwise disabled.
#
diff --git a/docs/sample_log_config.yaml b/docs/sample_log_config.yaml
index 2485ad25ed..3065a0e2d9 100644
--- a/docs/sample_log_config.yaml
+++ b/docs/sample_log_config.yaml
@@ -62,13 +62,6 @@ loggers:
# information such as access tokens.
level: INFO
- twisted:
- # We send the twisted logging directly to the file handler,
- # to work around https://github.com/matrix-org/synapse/issues/3471
- # when using "buffer" logger. Use "console" to log to stderr instead.
- handlers: [file]
- propagate: false
-
root:
level: INFO
diff --git a/docs/upgrade.md b/docs/upgrade.md
index 3a8aeb0395..fa4b3ef590 100644
--- a/docs/upgrade.md
+++ b/docs/upgrade.md
@@ -89,6 +89,50 @@ process, for example:
dpkg -i matrix-synapse-py3_1.3.0+stretch1_amd64.deb
```
+# Upgrading to v1.59.0
+
+## Device name lookup over federation has been disabled by default
+
+The names of user devices are no longer visible to users on other homeservers by default.
+Device IDs are unaffected, as these are necessary to facilitate end-to-end encryption.
+
+To re-enable this functionality, set the
+[`allow_device_name_lookup_over_federation`](https://matrix-org.github.io/synapse/v1.59/usage/configuration/config_documentation.html#federation)
+homeserver config option to `true`.
+
+
+## Deprecation of the `synapse.app.appservice` and `synapse.app.user_dir` worker application types
+
+The `synapse.app.appservice` worker application type allowed you to configure a
+single worker to use to notify application services of new events, as long
+as this functionality was disabled on the main process with `notify_appservices: False`.
+Further, the `synapse.app.user_dir` worker application type allowed you to configure
+a single worker to be responsible for updating the user directory, as long as this
+was disabled on the main process with `update_user_directory: False`.
+
+To unify Synapse's worker types, the `synapse.app.appservice` worker application
+type and the `notify_appservices` configuration option have been deprecated.
+The `synapse.app.user_dir` worker application type and `update_user_directory`
+configuration option have also been deprecated.
+
+To get the same functionality as was provided by the deprecated options, it's now recommended that the `synapse.app.generic_worker`
+worker application type is used and that the `notify_appservices_from_worker` and/or
+`update_user_directory_from_worker` options are set to the name of a worker.
+
+For the time being, the old options can be used alongside the new options to make
+it easier to transition between the two configurations, however please note that:
+
+- the options must not contradict each other (otherwise Synapse won't start); and
+- the `notify_appservices` and `update_user_directory` options will be removed in a future release of Synapse.
+
+Please see the [*Notifying Application Services*][v1_59_notify_ases_from] and
+[*Updating the User Directory*][v1_59_update_user_dir] sections of the worker
+documentation for more information.
+
+[v1_59_notify_ases_from]: workers.md#notifying-application-services
+[v1_59_update_user_dir]: workers.md#updating-the-user-directory
+
+
# Upgrading to v1.58.0
## Groups/communities feature has been disabled by default
@@ -96,6 +140,7 @@ process, for example:
The non-standard groups/communities feature in Synapse has been disabled by default
and will be removed in Synapse v1.61.0.
+
# Upgrading to v1.57.0
## Changes to database schema for application services
diff --git a/docs/usage/administration/request_log.md b/docs/usage/administration/request_log.md
index 316304c734..adb5f4f5f3 100644
--- a/docs/usage/administration/request_log.md
+++ b/docs/usage/administration/request_log.md
@@ -28,7 +28,7 @@ See the following for how to decode the dense data available from the default lo
| NNNN | Total time waiting for response to DB queries across all parallel DB work from this request |
| OOOO | Count of DB transactions performed |
| PPPP | Response body size |
-| QQQQ | Response status code (prefixed with ! if the socket was closed before the response was generated) |
+| QQQQ | Response status code
Suffixed with `!` if the socket was closed before the response was generated.
A `499!` status code indicates that Synapse also cancelled request processing after the socket was closed.
|
| RRRR | Request |
| SSSS | User-agent |
| TTTT | Events fetched from DB to service this request (note that this does not include events fetched from the cache) |
diff --git a/docs/usage/administration/useful_sql_for_admins.md b/docs/usage/administration/useful_sql_for_admins.md
index d4aada3272..f3b97f9576 100644
--- a/docs/usage/administration/useful_sql_for_admins.md
+++ b/docs/usage/administration/useful_sql_for_admins.md
@@ -1,7 +1,10 @@
## Some useful SQL queries for Synapse Admins
## Size of full matrix db
-`SELECT pg_size_pretty( pg_database_size( 'matrix' ) );`
+```sql
+SELECT pg_size_pretty( pg_database_size( 'matrix' ) );
+```
+
### Result example:
```
pg_size_pretty
@@ -9,39 +12,19 @@ pg_size_pretty
6420 MB
(1 row)
```
-## Show top 20 larger rooms by state events count
-```sql
-SELECT r.name, s.room_id, s.current_state_events
- FROM room_stats_current s
- LEFT JOIN room_stats_state r USING (room_id)
- ORDER BY current_state_events DESC
- LIMIT 20;
-```
-
-and by state_group_events count:
-```sql
-SELECT rss.name, s.room_id, count(s.room_id) FROM state_groups_state s
-LEFT JOIN room_stats_state rss USING (room_id)
-GROUP BY s.room_id, rss.name
-ORDER BY count(s.room_id) DESC
-LIMIT 20;
-```
-plus same, but with join removed for performance reasons:
-```sql
-SELECT s.room_id, count(s.room_id) FROM state_groups_state s
-GROUP BY s.room_id
-ORDER BY count(s.room_id) DESC
-LIMIT 20;
-```
## Show top 20 larger tables by row count
```sql
-SELECT relname, n_live_tup as rows
- FROM pg_stat_user_tables
+SELECT relname, n_live_tup AS "rows"
+ FROM pg_stat_user_tables
ORDER BY n_live_tup DESC
LIMIT 20;
```
-This query is quick, but may be very approximate, for exact number of rows use `SELECT COUNT(*) FROM `.
+This query is quick, but may be very approximate, for exact number of rows use:
+```sql
+SELECT COUNT(*) FROM ;
+```
+
### Result example:
```
state_groups_state - 161687170
@@ -66,46 +49,19 @@ device_lists_stream - 326903
user_directory_search - 316433
```
-## Show top 20 rooms by new events count in last 1 day:
-```sql
-SELECT e.room_id, r.name, COUNT(e.event_id) cnt FROM events e
-LEFT JOIN room_stats_state r USING (room_id)
-WHERE e.origin_server_ts >= DATE_PART('epoch', NOW() - INTERVAL '1 day') * 1000 GROUP BY e.room_id, r.name ORDER BY cnt DESC LIMIT 20;
-```
-
-## Show top 20 users on homeserver by sent events (messages) at last month:
-```sql
-SELECT user_id, SUM(total_events)
- FROM user_stats_historical
- WHERE TO_TIMESTAMP(end_ts/1000) AT TIME ZONE 'UTC' > date_trunc('day', now() - interval '1 month')
- GROUP BY user_id
- ORDER BY SUM(total_events) DESC
- LIMIT 20;
-```
-
-## Show last 100 messages from needed user, with room names:
-```sql
-SELECT e.room_id, r.name, e.event_id, e.type, e.content, j.json FROM events e
- LEFT JOIN event_json j USING (room_id)
- LEFT JOIN room_stats_state r USING (room_id)
- WHERE sender = '@LOGIN:example.com'
- AND e.type = 'm.room.message'
- ORDER BY stream_ordering DESC
- LIMIT 100;
-```
-
## Show top 20 larger tables by storage size
```sql
SELECT nspname || '.' || relname AS "relation",
- pg_size_pretty(pg_total_relation_size(C.oid)) AS "total_size"
- FROM pg_class C
- LEFT JOIN pg_namespace N ON (N.oid = C.relnamespace)
+ pg_size_pretty(pg_total_relation_size(c.oid)) AS "total_size"
+ FROM pg_class c
+ LEFT JOIN pg_namespace n ON (n.oid = c.relnamespace)
WHERE nspname NOT IN ('pg_catalog', 'information_schema')
- AND C.relkind <> 'i'
+ AND c.relkind <> 'i'
AND nspname !~ '^pg_toast'
- ORDER BY pg_total_relation_size(C.oid) DESC
+ ORDER BY pg_total_relation_size(c.oid) DESC
LIMIT 20;
```
+
### Result example:
```
public.state_groups_state - 27 GB
@@ -130,8 +86,93 @@ public.device_lists_remote_cache - 124 MB
public.state_group_edges - 122 MB
```
+## Show top 20 larger rooms by state events count
+You get the same information when you use the
+[admin API](../../admin_api/rooms.md#list-room-api)
+and set parameter `order_by=state_events`.
+
+```sql
+SELECT r.name, s.room_id, s.current_state_events
+ FROM room_stats_current s
+ LEFT JOIN room_stats_state r USING (room_id)
+ ORDER BY current_state_events DESC
+ LIMIT 20;
+```
+
+and by state_group_events count:
+```sql
+SELECT rss.name, s.room_id, COUNT(s.room_id)
+ FROM state_groups_state s
+ LEFT JOIN room_stats_state rss USING (room_id)
+ GROUP BY s.room_id, rss.name
+ ORDER BY COUNT(s.room_id) DESC
+ LIMIT 20;
+```
+
+plus same, but with join removed for performance reasons:
+```sql
+SELECT s.room_id, COUNT(s.room_id)
+ FROM state_groups_state s
+ GROUP BY s.room_id
+ ORDER BY COUNT(s.room_id) DESC
+ LIMIT 20;
+```
+
+## Show top 20 rooms by new events count in last 1 day:
+```sql
+SELECT e.room_id, r.name, COUNT(e.event_id) cnt
+ FROM events e
+ LEFT JOIN room_stats_state r USING (room_id)
+ WHERE e.origin_server_ts >= DATE_PART('epoch', NOW() - INTERVAL '1 day') * 1000
+ GROUP BY e.room_id, r.name
+ ORDER BY cnt DESC
+ LIMIT 20;
+```
+
+## Show top 20 users on homeserver by sent events (messages) at last month:
+Caution. This query does not use any indexes, can be slow and create load on the database.
+```sql
+SELECT COUNT(*), sender
+ FROM events
+ WHERE (type = 'm.room.encrypted' OR type = 'm.room.message')
+ AND origin_server_ts >= DATE_PART('epoch', NOW() - INTERVAL '1 month') * 1000
+ GROUP BY sender
+ ORDER BY COUNT(*) DESC
+ LIMIT 20;
+```
+
+## Show last 100 messages from needed user, with room names:
+```sql
+SELECT e.room_id, r.name, e.event_id, e.type, e.content, j.json
+ FROM events e
+ LEFT JOIN event_json j USING (room_id)
+ LEFT JOIN room_stats_state r USING (room_id)
+ WHERE sender = '@LOGIN:example.com'
+ AND e.type = 'm.room.message'
+ ORDER BY stream_ordering DESC
+ LIMIT 100;
+```
+
## Show rooms with names, sorted by events in this rooms
-`echo "select event_json.room_id,room_stats_state.name from event_json,room_stats_state where room_stats_state.room_id=event_json.room_id" | psql synapse | sort | uniq -c | sort -n`
+
+**Sort and order with bash**
+```bash
+echo "SELECT event_json.room_id, room_stats_state.name FROM event_json, room_stats_state \
+WHERE room_stats_state.room_id = event_json.room_id" | psql -d synapse -h localhost -U synapse_user -t \
+| sort | uniq -c | sort -n
+```
+Documentation for `psql` command line parameters: https://www.postgresql.org/docs/current/app-psql.html
+
+**Sort and order with SQL**
+```sql
+SELECT COUNT(*), event_json.room_id, room_stats_state.name
+ FROM event_json, room_stats_state
+ WHERE room_stats_state.room_id = event_json.room_id
+ GROUP BY event_json.room_id, room_stats_state.name
+ ORDER BY COUNT(*) DESC
+ LIMIT 50;
+```
+
### Result example:
```
9459 !FPUfgzXYWTKgIrwKxW:matrix.org | This Week in Matrix
@@ -145,12 +186,22 @@ public.state_group_edges - 122 MB
```
## Lookup room state info by list of room_id
+You get the same information when you use the
+[admin API](../../admin_api/rooms.md#room-details-api).
```sql
-SELECT rss.room_id, rss.name, rss.canonical_alias, rss.topic, rss.encryption, rsc.joined_members, rsc.local_users_in_room, rss.join_rules
-FROM room_stats_state rss
-LEFT JOIN room_stats_current rsc USING (room_id)
-WHERE room_id IN (WHERE room_id IN (
- '!OGEhHVWSdvArJzumhm:matrix.org',
- '!YTvKGNlinIzlkMTVRl:matrix.org'
-)
-```
\ No newline at end of file
+SELECT rss.room_id, rss.name, rss.canonical_alias, rss.topic, rss.encryption,
+ rsc.joined_members, rsc.local_users_in_room, rss.join_rules
+ FROM room_stats_state rss
+ LEFT JOIN room_stats_current rsc USING (room_id)
+ WHERE room_id IN ( WHERE room_id IN (
+ '!OGEhHVWSdvArJzumhm:matrix.org',
+ '!YTvKGNlinIzlkMTVRl:matrix.org'
+ );
+```
+
+## Show users and devices that have not been online for a while
+```sql
+SELECT user_id, device_id, user_agent, TO_TIMESTAMP(last_seen / 1000) AS "last_seen"
+ FROM devices
+ WHERE last_seen < DATE_PART('epoch', NOW() - INTERVAL '3 month') * 1000;
+```
diff --git a/docs/usage/configuration/config_documentation.md b/docs/usage/configuration/config_documentation.md
index 968b0fbfaf..21dad0ac41 100644
--- a/docs/usage/configuration/config_documentation.md
+++ b/docs/usage/configuration/config_documentation.md
@@ -627,6 +627,20 @@ Example configuration:
mau_trial_days: 5
```
---
+Config option: `mau_appservice_trial_days`
+
+The option `mau_appservice_trial_days` is similar to `mau_trial_days`, but applies a different
+trial number if the user was registered by an appservice. A value
+of 0 means no trial days are applied. Appservices not listed in this dictionary
+use the value of `mau_trial_days` instead.
+
+Example configuration:
+```yaml
+mau_appservice_trial_days:
+ my_appservice_id: 3
+ another_appservice_id: 6
+```
+---
Config option: `mau_limit_alerting`
The option `mau_limit_alerting` is a means of limiting client-side alerting
@@ -1035,13 +1049,13 @@ allow_profile_lookup_over_federation: false
---
Config option: `allow_device_name_lookup_over_federation`
-Set this option to false to disable device display name lookup over federation. By default, the
-Federation API allows other homeservers to obtain device display names of any user
+Set this option to true to allow device display name lookup over federation. By default, the
+Federation API prevents other homeservers from obtaining the display names of any user devices
on this homeserver.
Example configuration:
```yaml
-allow_device_name_lookup_over_federation: false
+allow_device_name_lookup_over_federation: true
```
---
## Caching ##
diff --git a/docs/workers.md b/docs/workers.md
index afdcd785e4..553792d238 100644
--- a/docs/workers.md
+++ b/docs/workers.md
@@ -426,7 +426,7 @@ the shared configuration would include:
run_background_tasks_on: background_worker
```
-You might also wish to investigate the `update_user_directory` and
+You might also wish to investigate the `update_user_directory_from_worker` and
`media_instance_running_background_jobs` settings.
An example for a dedicated background worker instance:
@@ -435,6 +435,40 @@ An example for a dedicated background worker instance:
{{#include systemd-with-workers/workers/background_worker.yaml}}
```
+#### Updating the User Directory
+
+You can designate one generic worker to update the user directory.
+
+Specify its name in the shared configuration as follows:
+
+```yaml
+update_user_directory_from_worker: worker_name
+```
+
+This work cannot be load-balanced; please ensure the main process is restarted
+after setting this option in the shared configuration!
+
+This style of configuration supersedes the legacy `synapse.app.user_dir`
+worker application type.
+
+
+#### Notifying Application Services
+
+You can designate one generic worker to send output traffic to Application Services.
+
+Specify its name in the shared configuration as follows:
+
+```yaml
+notify_appservices_from_worker: worker_name
+```
+
+This work cannot be load-balanced; please ensure the main process is restarted
+after setting this option in the shared configuration!
+
+This style of configuration supersedes the legacy `synapse.app.appservice`
+worker application type.
+
+
### `synapse.app.pusher`
Handles sending push notifications to sygnal and email. Doesn't handle any
@@ -453,6 +487,9 @@ pusher_instances:
### `synapse.app.appservice`
+**Deprecated as of Synapse v1.59.** [Use `synapse.app.generic_worker` with the
+`notify_appservices_from_worker` option instead.](#notifying-application-services)
+
Handles sending output traffic to Application Services. Doesn't handle any
REST endpoints itself, but you should set `notify_appservices: False` in the
shared configuration file to stop the main synapse sending appservice notifications.
@@ -520,6 +557,9 @@ Note that if a reverse proxy is used , then `/_matrix/media/` must be routed for
### `synapse.app.user_dir`
+**Deprecated as of Synapse v1.59.** [Use `synapse.app.generic_worker` with the
+`update_user_directory_from_worker` option instead.](#updating-the-user-directory)
+
Handles searches in the user directory. It can handle REST endpoints matching
the following regular expressions:
diff --git a/mypy.ini b/mypy.ini
index a663bf6975..ba0de419f5 100644
--- a/mypy.ini
+++ b/mypy.ini
@@ -7,6 +7,7 @@ show_error_codes = True
show_traceback = True
mypy_path = stubs
warn_unreachable = True
+warn_unused_ignores = True
local_partial_types = True
no_implicit_optional = True
@@ -23,10 +24,6 @@ files =
# https://docs.python.org/3/library/re.html#re.X
exclude = (?x)
^(
- |scripts-dev/build_debian_packages.py
- |scripts-dev/federation_client.py
- |scripts-dev/release.py
-
|synapse/storage/databases/__init__.py
|synapse/storage/databases/main/cache.py
|synapse/storage/databases/main/devices.py
@@ -134,6 +131,11 @@ disallow_untyped_defs = True
[mypy-synapse.metrics.*]
disallow_untyped_defs = True
+[mypy-synapse.metrics._reactor_metrics]
+# This module imports select.epoll. That exists on Linux, but doesn't on macOS.
+# See https://github.com/matrix-org/synapse/pull/11771.
+warn_unused_ignores = False
+
[mypy-synapse.module_api.*]
disallow_untyped_defs = True
@@ -239,63 +241,26 @@ disallow_untyped_defs = True
[mypy-authlib.*]
ignore_missing_imports = True
-[mypy-bcrypt]
-ignore_missing_imports = True
-
[mypy-canonicaljson]
ignore_missing_imports = True
-[mypy-constantly]
-ignore_missing_imports = True
-
-[mypy-daemonize]
-ignore_missing_imports = True
-
-[mypy-h11]
-ignore_missing_imports = True
-
-[mypy-hiredis]
-ignore_missing_imports = True
-
-[mypy-hyperlink]
-ignore_missing_imports = True
-
[mypy-ijson.*]
ignore_missing_imports = True
-[mypy-importlib_metadata.*]
-ignore_missing_imports = True
-
-[mypy-jaeger_client.*]
-ignore_missing_imports = True
-
-[mypy-josepy.*]
-ignore_missing_imports = True
-
-[mypy-jwt.*]
-ignore_missing_imports = True
-
[mypy-lxml]
ignore_missing_imports = True
[mypy-msgpack]
ignore_missing_imports = True
-[mypy-nacl.*]
-ignore_missing_imports = True
-
+# Note: WIP stubs available at
+# https://github.com/microsoft/python-type-stubs/tree/64934207f523ad6b611e6cfe039d85d7175d7d0d/netaddr
[mypy-netaddr]
ignore_missing_imports = True
[mypy-parameterized.*]
ignore_missing_imports = True
-[mypy-phonenumbers.*]
-ignore_missing_imports = True
-
-[mypy-prometheus_client.*]
-ignore_missing_imports = True
-
[mypy-pymacaroons.*]
ignore_missing_imports = True
@@ -308,23 +273,14 @@ ignore_missing_imports = True
[mypy-saml2.*]
ignore_missing_imports = True
-[mypy-sentry_sdk]
-ignore_missing_imports = True
-
[mypy-service_identity.*]
ignore_missing_imports = True
-[mypy-signedjson.*]
+[mypy-srvlookup.*]
ignore_missing_imports = True
[mypy-treq.*]
ignore_missing_imports = True
-[mypy-twisted.*]
-ignore_missing_imports = True
-
-[mypy-zope]
-ignore_missing_imports = True
-
[mypy-incremental.*]
ignore_missing_imports = True
diff --git a/poetry.lock b/poetry.lock
index 8c7af1fa1e..49a912a589 100644
--- a/poetry.lock
+++ b/poetry.lock
@@ -309,14 +309,15 @@ smmap = ">=3.0.1,<6"
[[package]]
name = "gitpython"
-version = "3.1.14"
-description = "Python Git Library"
+version = "3.1.27"
+description = "GitPython is a python library used to interact with Git repositories"
category = "dev"
optional = false
-python-versions = ">=3.4"
+python-versions = ">=3.7"
[package.dependencies]
gitdb = ">=4.0.1,<5"
+typing-extensions = {version = ">=3.7.4.3", markers = "python_version < \"3.8\""}
[[package]]
name = "hiredis"
@@ -571,7 +572,7 @@ python-versions = "*"
[[package]]
name = "mypy"
-version = "0.931"
+version = "0.950"
description = "Optional static typing for Python"
category = "dev"
optional = false
@@ -579,13 +580,14 @@ python-versions = ">=3.6"
[package.dependencies]
mypy-extensions = ">=0.4.3"
-tomli = ">=1.1.0"
+tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""}
typed-ast = {version = ">=1.4.0,<2", markers = "python_version < \"3.8\""}
typing-extensions = ">=3.10"
[package.extras]
dmypy = ["psutil (>=4.0)"]
python2 = ["typed-ast (>=1.4.0,<2)"]
+reports = ["lxml"]
[[package]]
name = "mypy-extensions"
@@ -597,14 +599,14 @@ python-versions = "*"
[[package]]
name = "mypy-zope"
-version = "0.3.5"
+version = "0.3.7"
description = "Plugin for mypy to support zope interfaces"
category = "dev"
optional = false
python-versions = "*"
[package.dependencies]
-mypy = "0.931"
+mypy = "0.950"
"zope.interface" = "*"
"zope.schema" = "*"
@@ -1019,7 +1021,7 @@ jeepney = ">=0.6"
[[package]]
name = "sentry-sdk"
-version = "1.5.7"
+version = "1.5.11"
description = "Python client for Sentry (https://sentry.io)"
category = "main"
optional = true
@@ -1315,6 +1317,14 @@ category = "dev"
optional = false
python-versions = "*"
+[[package]]
+name = "types-commonmark"
+version = "0.9.2"
+description = "Typing stubs for commonmark"
+category = "dev"
+optional = false
+python-versions = "*"
+
[[package]]
name = "types-cryptography"
version = "3.3.15"
@@ -1361,7 +1371,7 @@ python-versions = "*"
[[package]]
name = "types-pillow"
-version = "9.0.6"
+version = "9.0.15"
description = "Typing stubs for Pillow"
category = "dev"
optional = false
@@ -1536,7 +1546,7 @@ docs = ["sphinx", "repoze.sphinx.autointerface"]
test = ["zope.i18nmessageid", "zope.testing", "zope.testrunner"]
[extras]
-all = ["matrix-synapse-ldap3", "psycopg2", "psycopg2cffi", "psycopg2cffi-compat", "pysaml2", "authlib", "lxml", "sentry-sdk", "jaeger-client", "opentracing", "pyjwt", "txredisapi", "hiredis"]
+all = ["matrix-synapse-ldap3", "psycopg2", "psycopg2cffi", "psycopg2cffi-compat", "pysaml2", "authlib", "lxml", "sentry-sdk", "jaeger-client", "opentracing", "pyjwt", "txredisapi", "hiredis", "Pympler"]
cache_memory = ["Pympler"]
jwt = ["pyjwt"]
matrix-synapse-ldap3 = ["matrix-synapse-ldap3"]
@@ -1552,8 +1562,8 @@ url_preview = ["lxml"]
[metadata]
lock-version = "1.1"
-python-versions = "^3.7"
-content-hash = "f482a4f594a165dfe01ce253a22510d5faf38647ab0dcebc35789350cafd9bf0"
+python-versions = "^3.7.1"
+content-hash = "d39d5ac5d51c014581186b7691999b861058b569084c525523baf70b77f292b1"
[metadata.files]
attrs = [
@@ -1766,8 +1776,8 @@ gitdb = [
{file = "gitdb-4.0.9.tar.gz", hash = "sha256:bac2fd45c0a1c9cf619e63a90d62bdc63892ef92387424b855792a6cabe789aa"},
]
gitpython = [
- {file = "GitPython-3.1.14-py3-none-any.whl", hash = "sha256:3283ae2fba31c913d857e12e5ba5f9a7772bbc064ae2bb09efafa71b0dd4939b"},
- {file = "GitPython-3.1.14.tar.gz", hash = "sha256:be27633e7509e58391f10207cd32b2a6cf5b908f92d9cd30da2e514e1137af61"},
+ {file = "GitPython-3.1.27-py3-none-any.whl", hash = "sha256:5b68b000463593e05ff2b261acff0ff0972df8ab1b70d3cdbd41b546c8b8fc3d"},
+ {file = "GitPython-3.1.27.tar.gz", hash = "sha256:1c885ce809e8ba2d88a29befeb385fcea06338d3640712b59ca623c220bb5704"},
]
hiredis = [
{file = "hiredis-2.0.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:b4c8b0bc5841e578d5fb32a16e0c305359b987b850a06964bd5a62739d688048"},
@@ -2080,34 +2090,37 @@ msgpack = [
{file = "msgpack-1.0.3.tar.gz", hash = "sha256:51fdc7fb93615286428ee7758cecc2f374d5ff363bdd884c7ea622a7a327a81e"},
]
mypy = [
- {file = "mypy-0.931-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:3c5b42d0815e15518b1f0990cff7a705805961613e701db60387e6fb663fe78a"},
- {file = "mypy-0.931-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c89702cac5b302f0c5d33b172d2b55b5df2bede3344a2fbed99ff96bddb2cf00"},
- {file = "mypy-0.931-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:300717a07ad09525401a508ef5d105e6b56646f7942eb92715a1c8d610149714"},
- {file = "mypy-0.931-cp310-cp310-win_amd64.whl", hash = "sha256:7b3f6f557ba4afc7f2ce6d3215d5db279bcf120b3cfd0add20a5d4f4abdae5bc"},
- {file = "mypy-0.931-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:1bf752559797c897cdd2c65f7b60c2b6969ffe458417b8d947b8340cc9cec08d"},
- {file = "mypy-0.931-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:4365c60266b95a3f216a3047f1d8e3f895da6c7402e9e1ddfab96393122cc58d"},
- {file = "mypy-0.931-cp36-cp36m-win_amd64.whl", hash = "sha256:1b65714dc296a7991000b6ee59a35b3f550e0073411ac9d3202f6516621ba66c"},
- {file = "mypy-0.931-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:e839191b8da5b4e5d805f940537efcaa13ea5dd98418f06dc585d2891d228cf0"},
- {file = "mypy-0.931-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:50c7346a46dc76a4ed88f3277d4959de8a2bd0a0fa47fa87a4cde36fe247ac05"},
- {file = "mypy-0.931-cp37-cp37m-win_amd64.whl", hash = "sha256:d8f1ff62f7a879c9fe5917b3f9eb93a79b78aad47b533911b853a757223f72e7"},
- {file = "mypy-0.931-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:f9fe20d0872b26c4bba1c1be02c5340de1019530302cf2dcc85c7f9fc3252ae0"},
- {file = "mypy-0.931-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:1b06268df7eb53a8feea99cbfff77a6e2b205e70bf31743e786678ef87ee8069"},
- {file = "mypy-0.931-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:8c11003aaeaf7cc2d0f1bc101c1cc9454ec4cc9cb825aef3cafff8a5fdf4c799"},
- {file = "mypy-0.931-cp38-cp38-win_amd64.whl", hash = "sha256:d9d2b84b2007cea426e327d2483238f040c49405a6bf4074f605f0156c91a47a"},
- {file = "mypy-0.931-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:ff3bf387c14c805ab1388185dd22d6b210824e164d4bb324b195ff34e322d166"},
- {file = "mypy-0.931-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:5b56154f8c09427bae082b32275a21f500b24d93c88d69a5e82f3978018a0266"},
- {file = "mypy-0.931-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:8ca7f8c4b1584d63c9a0f827c37ba7a47226c19a23a753d52e5b5eddb201afcd"},
- {file = "mypy-0.931-cp39-cp39-win_amd64.whl", hash = "sha256:74f7eccbfd436abe9c352ad9fb65872cc0f1f0a868e9d9c44db0893440f0c697"},
- {file = "mypy-0.931-py3-none-any.whl", hash = "sha256:1171f2e0859cfff2d366da2c7092b06130f232c636a3f7301e3feb8b41f6377d"},
- {file = "mypy-0.931.tar.gz", hash = "sha256:0038b21890867793581e4cb0d810829f5fd4441aa75796b53033af3aa30430ce"},
+ {file = "mypy-0.950-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:cf9c261958a769a3bd38c3e133801ebcd284ffb734ea12d01457cb09eacf7d7b"},
+ {file = "mypy-0.950-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:b5b5bd0ffb11b4aba2bb6d31b8643902c48f990cc92fda4e21afac658044f0c0"},
+ {file = "mypy-0.950-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:5e7647df0f8fc947388e6251d728189cfadb3b1e558407f93254e35abc026e22"},
+ {file = "mypy-0.950-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:eaff8156016487c1af5ffa5304c3e3fd183edcb412f3e9c72db349faf3f6e0eb"},
+ {file = "mypy-0.950-cp310-cp310-win_amd64.whl", hash = "sha256:563514c7dc504698fb66bb1cf897657a173a496406f1866afae73ab5b3cdb334"},
+ {file = "mypy-0.950-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:dd4d670eee9610bf61c25c940e9ade2d0ed05eb44227275cce88701fee014b1f"},
+ {file = "mypy-0.950-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:ca75ecf2783395ca3016a5e455cb322ba26b6d33b4b413fcdedfc632e67941dc"},
+ {file = "mypy-0.950-cp36-cp36m-win_amd64.whl", hash = "sha256:6003de687c13196e8a1243a5e4bcce617d79b88f83ee6625437e335d89dfebe2"},
+ {file = "mypy-0.950-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:4c653e4846f287051599ed8f4b3c044b80e540e88feec76b11044ddc5612ffed"},
+ {file = "mypy-0.950-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:e19736af56947addedce4674c0971e5dceef1b5ec7d667fe86bcd2b07f8f9075"},
+ {file = "mypy-0.950-cp37-cp37m-win_amd64.whl", hash = "sha256:ef7beb2a3582eb7a9f37beaf38a28acfd801988cde688760aea9e6cc4832b10b"},
+ {file = "mypy-0.950-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:0112752a6ff07230f9ec2f71b0d3d4e088a910fdce454fdb6553e83ed0eced7d"},
+ {file = "mypy-0.950-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:ee0a36edd332ed2c5208565ae6e3a7afc0eabb53f5327e281f2ef03a6bc7687a"},
+ {file = "mypy-0.950-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:77423570c04aca807508a492037abbd72b12a1fb25a385847d191cd50b2c9605"},
+ {file = "mypy-0.950-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:5ce6a09042b6da16d773d2110e44f169683d8cc8687e79ec6d1181a72cb028d2"},
+ {file = "mypy-0.950-cp38-cp38-win_amd64.whl", hash = "sha256:5b231afd6a6e951381b9ef09a1223b1feabe13625388db48a8690f8daa9b71ff"},
+ {file = "mypy-0.950-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:0384d9f3af49837baa92f559d3fa673e6d2652a16550a9ee07fc08c736f5e6f8"},
+ {file = "mypy-0.950-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:1fdeb0a0f64f2a874a4c1f5271f06e40e1e9779bf55f9567f149466fc7a55038"},
+ {file = "mypy-0.950-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:61504b9a5ae166ba5ecfed9e93357fd51aa693d3d434b582a925338a2ff57fd2"},
+ {file = "mypy-0.950-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:a952b8bc0ae278fc6316e6384f67bb9a396eb30aced6ad034d3a76120ebcc519"},
+ {file = "mypy-0.950-cp39-cp39-win_amd64.whl", hash = "sha256:eaea21d150fb26d7b4856766e7addcf929119dd19fc832b22e71d942835201ef"},
+ {file = "mypy-0.950-py3-none-any.whl", hash = "sha256:a4d9898f46446bfb6405383b57b96737dcfd0a7f25b748e78ef3e8c576bba3cb"},
+ {file = "mypy-0.950.tar.gz", hash = "sha256:1b333cfbca1762ff15808a0ef4f71b5d3eed8528b23ea1c3fb50543c867d68de"},
]
mypy-extensions = [
{file = "mypy_extensions-0.4.3-py2.py3-none-any.whl", hash = "sha256:090fedd75945a69ae91ce1303b5824f428daf5a028d2f6ab8a299250a846f15d"},
{file = "mypy_extensions-0.4.3.tar.gz", hash = "sha256:2d82818f5bb3e369420cb3c4060a7970edba416647068eb4c5343488a6c604a8"},
]
mypy-zope = [
- {file = "mypy-zope-0.3.5.tar.gz", hash = "sha256:489e7da1c2af887f2cfe3496995fc247f296512b495b57817edddda9d22308f3"},
- {file = "mypy_zope-0.3.5-py3-none-any.whl", hash = "sha256:3bd0cc9a3e5933b02931af4b214ba32a4f4ff98adb30c979ce733857db91a18b"},
+ {file = "mypy-zope-0.3.7.tar.gz", hash = "sha256:9da171e78e8ef7ac8922c86af1a62f1b7f3244f121020bd94a2246bc3f33c605"},
+ {file = "mypy_zope-0.3.7-py3-none-any.whl", hash = "sha256:9c7637d066e4d1bafa0651abc091c752009769098043b236446e6725be2bc9c2"},
]
netaddr = [
{file = "netaddr-0.8.0-py2.py3-none-any.whl", hash = "sha256:9666d0232c32d2656e5e5f8d735f58fd6c7457ce52fc21c98d45f2af78f990ac"},
@@ -2377,8 +2390,8 @@ secretstorage = [
{file = "SecretStorage-3.3.1.tar.gz", hash = "sha256:fd666c51a6bf200643495a04abb261f83229dcb6fd8472ec393df7ffc8b6f195"},
]
sentry-sdk = [
- {file = "sentry-sdk-1.5.7.tar.gz", hash = "sha256:aa52da941c56b5a76fd838f8e9e92a850bf893a9eb1e33ffce6c21431d07ee30"},
- {file = "sentry_sdk-1.5.7-py2.py3-none-any.whl", hash = "sha256:411a8495bd18cf13038e5749e4710beb4efa53da6351f67b4c2f307c2d9b6d49"},
+ {file = "sentry-sdk-1.5.11.tar.gz", hash = "sha256:6c01d9d0b65935fd275adc120194737d1df317dce811e642cbf0394d0d37a007"},
+ {file = "sentry_sdk-1.5.11-py2.py3-none-any.whl", hash = "sha256:c17179183cac614e900cbd048dab03f49a48e2820182ec686c25e7ce46f8548f"},
]
service-identity = [
{file = "service-identity-21.1.0.tar.gz", hash = "sha256:6e6c6086ca271dc11b033d17c3a8bea9f24ebff920c587da090afc9519419d34"},
@@ -2588,6 +2601,10 @@ types-bleach = [
{file = "types-bleach-4.1.4.tar.gz", hash = "sha256:2d30c2c4fb6854088ac636471352c9a51bf6c089289800d2a8060820a01cd43a"},
{file = "types_bleach-4.1.4-py3-none-any.whl", hash = "sha256:edffe173ed6d7b6f3543036a96204a9319c3bf6c3645917b14274e43f000cc9b"},
]
+types-commonmark = [
+ {file = "types-commonmark-0.9.2.tar.gz", hash = "sha256:b894b67750c52fd5abc9a40a9ceb9da4652a391d75c1b480bba9cef90f19fc86"},
+ {file = "types_commonmark-0.9.2-py3-none-any.whl", hash = "sha256:56f20199a1f9a2924443211a0ef97f8b15a8a956a7f4e9186be6950bf38d6d02"},
+]
types-cryptography = [
{file = "types-cryptography-3.3.15.tar.gz", hash = "sha256:a7983a75a7b88a18f88832008f0ef140b8d1097888ec1a0824ec8fb7e105273b"},
{file = "types_cryptography-3.3.15-py3-none-any.whl", hash = "sha256:d9b0dd5465d7898d400850e7f35e5518aa93a7e23d3e11757cd81b4777089046"},
@@ -2609,8 +2626,8 @@ types-opentracing = [
{file = "types_opentracing-2.4.7-py3-none-any.whl", hash = "sha256:861fb8103b07cf717f501dd400cb274ca9992552314d4d6c7a824b11a215e512"},
]
types-pillow = [
- {file = "types-Pillow-9.0.6.tar.gz", hash = "sha256:79b350b1188c080c27558429f1e119e69c9f020b877a82df761d9283070e0185"},
- {file = "types_Pillow-9.0.6-py3-none-any.whl", hash = "sha256:bd1e0a844fc718398aa265bf50fcad550fc520cc54f80e5ffeb7b3226b3cc507"},
+ {file = "types-Pillow-9.0.15.tar.gz", hash = "sha256:d2e385fe5c192e75970f18accce69f5c2a9f186f3feb578a9b91cd6fdf64211d"},
+ {file = "types_Pillow-9.0.15-py3-none-any.whl", hash = "sha256:c9646595dfafdf8b63d4b1443292ead17ee0fc7b18a143e497b68e0ea2dc1eb6"},
]
types-psycopg2 = [
{file = "types-psycopg2-2.9.9.tar.gz", hash = "sha256:4f9d4d52eeb343dc00fd5ed4f1513a8a5c18efba0a072eb82706d15cf4f20a2e"},
diff --git a/pyproject.toml b/pyproject.toml
index bdded78434..e3d81ae5f1 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -54,7 +54,7 @@ skip_gitignore = true
[tool.poetry]
name = "matrix-synapse"
-version = "1.58.0rc2"
+version = "1.59.0rc1"
description = "Homeserver for the Matrix decentralised comms protocol"
authors = ["Matrix.org Team and Contributors "]
license = "Apache-2.0"
@@ -100,7 +100,7 @@ synapse_review_recent_signups = "synapse._scripts.review_recent_signups:main"
update_synapse_database = "synapse._scripts.update_synapse_database:main"
[tool.poetry.dependencies]
-python = "^3.7"
+python = "^3.7.1"
# Mandatory Dependencies
# ----------------------
@@ -142,8 +142,10 @@ netaddr = ">=0.7.18"
# add a lower bound to the Jinja2 dependency.
Jinja2 = ">=3.0"
bleach = ">=1.4.3"
-# We use `ParamSpec`, which was added in `typing-extensions` 3.10.0.0.
-typing-extensions = ">=3.10.0"
+# We use `ParamSpec` and `Concatenate`, which were added in `typing-extensions` 3.10.0.0.
+# Additionally we need https://github.com/python/typing/pull/817 to allow types to be
+# generic over ParamSpecs.
+typing-extensions = ">=3.10.0.1"
# We enforce that we have a `cryptography` version that bundles an `openssl`
# with the latest security patches.
cryptography = ">=3.4.7"
@@ -231,10 +233,11 @@ all = [
"jaeger-client", "opentracing",
# jwt
"pyjwt",
- #redis
- "txredisapi", "hiredis"
+ # redis
+ "txredisapi", "hiredis",
+ # cache_memory
+ "pympler",
# omitted:
- # - cache_memory: this is an experimental option
# - test: it's useful to have this separate from dev deps in the olddeps job
# - systemd: this is a system-based requirement
]
@@ -248,9 +251,10 @@ flake8-bugbear = "==21.3.2"
flake8 = "*"
# Typechecking
-mypy = "==0.931"
-mypy-zope = "==0.3.5"
+mypy = "*"
+mypy-zope = "*"
types-bleach = ">=4.1.0"
+types-commonmark = ">=0.9.2"
types-jsonschema = ">=3.2.0"
types-opentracing = ">=2.4.2"
types-Pillow = ">=8.3.4"
@@ -270,7 +274,8 @@ idna = ">=2.5"
# The following are used by the release script
click = "==8.1.0"
-GitPython = "==3.1.14"
+# GitPython was == 3.1.14; bumped to 3.1.20, the first release with type hints.
+GitPython = ">=3.1.20"
commonmark = "==0.9.1"
pygithub = "==1.55"
# The following are executed as commands by the release script.
diff --git a/scripts-dev/build_debian_packages.py b/scripts-dev/build_debian_packages.py
index e3e6878686..38564893e9 100755
--- a/scripts-dev/build_debian_packages.py
+++ b/scripts-dev/build_debian_packages.py
@@ -17,7 +17,8 @@ import subprocess
import sys
import threading
from concurrent.futures import ThreadPoolExecutor
-from typing import Optional, Sequence
+from types import FrameType
+from typing import Collection, Optional, Sequence, Set
DISTS = (
"debian:buster", # oldstable: EOL 2022-08
@@ -41,15 +42,17 @@ projdir = os.path.dirname(os.path.dirname(os.path.realpath(__file__)))
class Builder(object):
def __init__(
- self, redirect_stdout=False, docker_build_args: Optional[Sequence[str]] = None
+ self,
+ redirect_stdout: bool = False,
+ docker_build_args: Optional[Sequence[str]] = None,
):
self.redirect_stdout = redirect_stdout
self._docker_build_args = tuple(docker_build_args or ())
- self.active_containers = set()
+ self.active_containers: Set[str] = set()
self._lock = threading.Lock()
self._failed = False
- def run_build(self, dist, skip_tests=False):
+ def run_build(self, dist: str, skip_tests: bool = False) -> None:
"""Build deb for a single distribution"""
if self._failed:
@@ -63,7 +66,7 @@ class Builder(object):
self._failed = True
raise
- def _inner_build(self, dist, skip_tests=False):
+ def _inner_build(self, dist: str, skip_tests: bool = False) -> None:
tag = dist.split(":", 1)[1]
# Make the dir where the debs will live.
@@ -138,7 +141,7 @@ class Builder(object):
stdout.close()
print("Completed build of %s" % (dist,))
- def kill_containers(self):
+ def kill_containers(self) -> None:
with self._lock:
active = list(self.active_containers)
@@ -156,8 +159,10 @@ class Builder(object):
self.active_containers.remove(c)
-def run_builds(builder, dists, jobs=1, skip_tests=False):
- def sig(signum, _frame):
+def run_builds(
+ builder: Builder, dists: Collection[str], jobs: int = 1, skip_tests: bool = False
+) -> None:
+ def sig(signum: int, _frame: Optional[FrameType]) -> None:
print("Caught SIGINT")
builder.kill_containers()
diff --git a/scripts-dev/complement.sh b/scripts-dev/complement.sh
index e0feba05fa..190df6909a 100755
--- a/scripts-dev/complement.sh
+++ b/scripts-dev/complement.sh
@@ -43,6 +43,8 @@ fi
# Build the base Synapse image from the local checkout
docker build -t matrixdotorg/synapse -f "docker/Dockerfile" .
+extra_test_args=()
+
# If we're using workers, modify the docker files slightly.
if [[ -n "$WORKERS" ]]; then
# Build the workers docker image (from the base Synapse image).
@@ -52,7 +54,14 @@ if [[ -n "$WORKERS" ]]; then
COMPLEMENT_DOCKERFILE=SynapseWorkers.Dockerfile
# And provide some more configuration to complement.
- export COMPLEMENT_SPAWN_HS_TIMEOUT_SECS=60
+
+ # It can take quite a while to spin up a worker-mode Synapse for the first
+ # time (the main problem is that we start 14 python processes for each test,
+ # and complement likes to do two of them in parallel).
+ export COMPLEMENT_SPAWN_HS_TIMEOUT_SECS=120
+
+ # ... and it takes longer than 10m to run the whole suite.
+ extra_test_args+=("-timeout=60m")
else
export COMPLEMENT_BASE_IMAGE=complement-synapse
COMPLEMENT_DOCKERFILE=Dockerfile
@@ -64,4 +73,4 @@ docker build -t $COMPLEMENT_BASE_IMAGE -f "docker/complement/$COMPLEMENT_DOCKERF
# Run the tests!
echo "Images built; running complement"
cd "$COMPLEMENT_DIR"
-go test -v -tags synapse_blacklist,msc2716,msc3030,faster_joins -count=1 "$@" ./tests/...
+go test -v -tags synapse_blacklist,msc2716,msc3030,faster_joins -count=1 "${extra_test_args[@]}" "$@" ./tests/...
diff --git a/scripts-dev/federation_client.py b/scripts-dev/federation_client.py
index 079d2f5ed0..763dd02c47 100755
--- a/scripts-dev/federation_client.py
+++ b/scripts-dev/federation_client.py
@@ -38,7 +38,7 @@ import argparse
import base64
import json
import sys
-from typing import Any, Optional
+from typing import Any, Dict, Optional, Tuple
from urllib import parse as urlparse
import requests
@@ -47,13 +47,14 @@ import signedjson.types
import srvlookup
import yaml
from requests.adapters import HTTPAdapter
+from urllib3 import HTTPConnectionPool
# uncomment the following to enable debug logging of http requests
# from httplib import HTTPConnection
# HTTPConnection.debuglevel = 1
-def encode_base64(input_bytes):
+def encode_base64(input_bytes: bytes) -> str:
"""Encode bytes as a base64 string without any padding."""
input_len = len(input_bytes)
@@ -63,7 +64,7 @@ def encode_base64(input_bytes):
return output_string
-def encode_canonical_json(value):
+def encode_canonical_json(value: object) -> bytes:
return json.dumps(
value,
# Encode code-points outside of ASCII as UTF-8 rather than \u escapes
@@ -130,7 +131,7 @@ def request(
sig,
destination,
)
- authorization_headers.append(header.encode("ascii"))
+ authorization_headers.append(header)
print("Authorization: %s" % header, file=sys.stderr)
dest = "matrix://%s%s" % (destination, path)
@@ -139,7 +140,10 @@ def request(
s = requests.Session()
s.mount("matrix://", MatrixConnectionAdapter())
- headers = {"Host": destination, "Authorization": authorization_headers[0]}
+ headers: Dict[str, str] = {
+ "Host": destination,
+ "Authorization": authorization_headers[0],
+ }
if method == "POST":
headers["Content-Type"] = "application/json"
@@ -154,7 +158,7 @@ def request(
)
-def main():
+def main() -> None:
parser = argparse.ArgumentParser(
description="Signs and sends a federation request to a matrix homeserver"
)
@@ -212,6 +216,7 @@ def main():
if not args.server_name or not args.signing_key:
read_args_from_config(args)
+ assert isinstance(args.signing_key, str)
algorithm, version, key_base64 = args.signing_key.split()
key = signedjson.key.decode_signing_key_base64(algorithm, version, key_base64)
@@ -233,7 +238,7 @@ def main():
print("")
-def read_args_from_config(args):
+def read_args_from_config(args: argparse.Namespace) -> None:
with open(args.config, "r") as fh:
config = yaml.safe_load(fh)
@@ -250,7 +255,7 @@ def read_args_from_config(args):
class MatrixConnectionAdapter(HTTPAdapter):
@staticmethod
- def lookup(s, skip_well_known=False):
+ def lookup(s: str, skip_well_known: bool = False) -> Tuple[str, int]:
if s[-1] == "]":
# ipv6 literal (with no port)
return s, 8448
@@ -276,7 +281,7 @@ class MatrixConnectionAdapter(HTTPAdapter):
return s, 8448
@staticmethod
- def get_well_known(server_name):
+ def get_well_known(server_name: str) -> Optional[str]:
uri = "https://%s/.well-known/matrix/server" % (server_name,)
print("fetching %s" % (uri,), file=sys.stderr)
@@ -299,7 +304,9 @@ class MatrixConnectionAdapter(HTTPAdapter):
print("Invalid response from %s: %s" % (uri, e), file=sys.stderr)
return None
- def get_connection(self, url, proxies=None):
+ def get_connection(
+ self, url: str, proxies: Optional[Dict[str, str]] = None
+ ) -> HTTPConnectionPool:
parsed = urlparse.urlparse(url)
(host, port) = self.lookup(parsed.netloc)
diff --git a/scripts-dev/mypy_synapse_plugin.py b/scripts-dev/mypy_synapse_plugin.py
index 1217e14874..c775865212 100644
--- a/scripts-dev/mypy_synapse_plugin.py
+++ b/scripts-dev/mypy_synapse_plugin.py
@@ -16,7 +16,7 @@
can crop up, e.g the cache descriptors.
"""
-from typing import Callable, Optional
+from typing import Callable, Optional, Type
from mypy.nodes import ARG_NAMED_OPT
from mypy.plugin import MethodSigContext, Plugin
@@ -94,7 +94,7 @@ def cached_function_method_signature(ctx: MethodSigContext) -> CallableType:
return signature
-def plugin(version: str):
+def plugin(version: str) -> Type[SynapsePlugin]:
# This is the entry point of the plugin, and let's us deal with the fact
# that the mypy plugin interface is *not* stable by looking at the version
# string.
diff --git a/scripts-dev/release.py b/scripts-dev/release.py
index 9d7c7c445f..0031ba3e4b 100755
--- a/scripts-dev/release.py
+++ b/scripts-dev/release.py
@@ -25,7 +25,7 @@ import sys
import urllib.request
from os import path
from tempfile import TemporaryDirectory
-from typing import List, Optional
+from typing import Any, List, Optional, cast
import attr
import click
@@ -36,7 +36,9 @@ from github import Github
from packaging import version
-def run_until_successful(command, *args, **kwargs):
+def run_until_successful(
+ command: str, *args: Any, **kwargs: Any
+) -> subprocess.CompletedProcess:
while True:
completed_process = subprocess.run(command, *args, **kwargs)
exit_code = completed_process.returncode
@@ -50,7 +52,7 @@ def run_until_successful(command, *args, **kwargs):
@click.group()
-def cli():
+def cli() -> None:
"""An interactive script to walk through the parts of creating a release.
Requires the dev dependencies be installed, which can be done via:
@@ -81,19 +83,13 @@ def cli():
@cli.command()
-def prepare():
+def prepare() -> None:
"""Do the initial stages of creating a release, including creating release
branch, updating changelog and pushing to GitHub.
"""
# Make sure we're in a git repo.
- try:
- repo = git.Repo()
- except git.InvalidGitRepositoryError:
- raise click.ClickException("Not in Synapse repo.")
-
- if repo.is_dirty():
- raise click.ClickException("Uncommitted changes exist.")
+ repo = get_repo_and_check_clean_checkout()
click.secho("Updating git repo...")
repo.remote().fetch()
@@ -161,22 +157,21 @@ def prepare():
click.get_current_context().abort()
# Switch to the release branch.
- parsed_new_version: version.Version = version.parse(new_version)
+ # Cast safety: parse() won't return a version.LegacyVersion from our
+ # version string format.
+ parsed_new_version = cast(version.Version, version.parse(new_version))
# We assume for debian changelogs that we only do RCs or full releases.
assert not parsed_new_version.is_devrelease
assert not parsed_new_version.is_postrelease
- release_branch_name = (
- f"release-v{parsed_new_version.major}.{parsed_new_version.minor}"
- )
+ release_branch_name = get_release_branch_name(parsed_new_version)
release_branch = find_ref(repo, release_branch_name)
if release_branch:
if release_branch.is_remote():
# If the release branch only exists on the remote we check it out
# locally.
repo.git.checkout(release_branch_name)
- release_branch = repo.active_branch
else:
# If a branch doesn't exist we create one. We ask which one branch it
# should be based off, defaulting to sensible values depending on the
@@ -198,13 +193,15 @@ def prepare():
click.get_current_context().abort()
# Check out the base branch and ensure it's up to date
- repo.head.reference = base_branch
+ repo.head.set_reference(base_branch, "check out the base branch")
repo.head.reset(index=True, working_tree=True)
if not base_branch.is_remote():
update_branch(repo)
# Create the new release branch
- release_branch = repo.create_head(release_branch_name, commit=base_branch)
+ # Type ignore will no longer be needed after GitPython 3.1.28.
+ # See https://github.com/gitpython-developers/GitPython/pull/1419
+ repo.create_head(release_branch_name, commit=base_branch) # type: ignore[arg-type]
# Switch to the release branch and ensure it's up to date.
repo.git.checkout(release_branch_name)
@@ -265,17 +262,11 @@ def prepare():
@cli.command()
@click.option("--gh-token", envvar=["GH_TOKEN", "GITHUB_TOKEN"])
-def tag(gh_token: Optional[str]):
+def tag(gh_token: Optional[str]) -> None:
"""Tags the release and generates a draft GitHub release"""
# Make sure we're in a git repo.
- try:
- repo = git.Repo()
- except git.InvalidGitRepositoryError:
- raise click.ClickException("Not in Synapse repo.")
-
- if repo.is_dirty():
- raise click.ClickException("Uncommitted changes exist.")
+ repo = get_repo_and_check_clean_checkout()
click.secho("Updating git repo...")
repo.remote().fetch()
@@ -288,12 +279,26 @@ def tag(gh_token: Optional[str]):
if tag_name in repo.tags:
raise click.ClickException(f"Tag {tag_name} already exists!\n")
+ # Check we're on the right release branch
+ release_branch = get_release_branch_name(current_version)
+ if repo.active_branch.name != release_branch:
+ click.echo(
+ f"Need to be on the release branch ({release_branch}) before tagging. "
+ f"Currently on ({repo.active_branch.name})."
+ )
+ click.get_current_context().abort()
+
# Get the appropriate changelogs and tag.
changes = get_changes_for_version(current_version)
click.echo_via_pager(changes)
if click.confirm("Edit text?", default=False):
- changes = click.edit(changes, require_save=False)
+ edited_changes = click.edit(changes, require_save=False)
+ # This assert is for mypy's benefit. click's docs are a little unclear, but
+ # when `require_save=False`, not saving the temp file in the editor returns
+ # the original string.
+ assert edited_changes is not None
+ changes = edited_changes
repo.create_tag(tag_name, message=changes, sign=True)
@@ -347,22 +352,16 @@ def tag(gh_token: Optional[str]):
@cli.command()
@click.option("--gh-token", envvar=["GH_TOKEN", "GITHUB_TOKEN"], required=True)
-def publish(gh_token: str):
- """Publish release."""
+def publish(gh_token: str) -> None:
+ """Publish release on GitHub."""
# Make sure we're in a git repo.
- try:
- repo = git.Repo()
- except git.InvalidGitRepositoryError:
- raise click.ClickException("Not in Synapse repo.")
-
- if repo.is_dirty():
- raise click.ClickException("Uncommitted changes exist.")
+ get_repo_and_check_clean_checkout()
current_version = get_package_version()
tag_name = f"v{current_version}"
- if not click.confirm(f"Publish {tag_name}?", default=True):
+ if not click.confirm(f"Publish release {tag_name} on GitHub?", default=True):
return
# Publish the draft release
@@ -390,12 +389,19 @@ def publish(gh_token: str):
@cli.command()
-def upload():
+def upload() -> None:
"""Upload release to pypi."""
current_version = get_package_version()
tag_name = f"v{current_version}"
+ # Check we have the right tag checked out.
+ repo = get_repo_and_check_clean_checkout()
+ tag = repo.tag(f"refs/tags/{tag_name}")
+ if repo.head.commit != tag.commit:
+ click.echo("Tag {tag_name} (tag.commit) is not currently checked out!")
+ click.get_current_context().abort()
+
pypi_asset_names = [
f"matrix_synapse-{current_version}-py3-none-any.whl",
f"matrix-synapse-{current_version}.tar.gz",
@@ -418,7 +424,7 @@ def upload():
@cli.command()
-def announce():
+def announce() -> None:
"""Generate markdown to announce the release."""
current_version = get_package_version()
@@ -428,7 +434,7 @@ def announce():
f"""
Hi everyone. Synapse {current_version} has just been released.
-[notes](https://github.com/matrix-org/synapse/releases/tag/{tag_name}) |\
+[notes](https://github.com/matrix-org/synapse/releases/tag/{tag_name}) | \
[docker](https://hub.docker.com/r/matrixdotorg/synapse/tags?name={tag_name}) | \
[debs](https://packages.matrix.org/debian/) | \
[pypi](https://pypi.org/project/matrix-synapse/{current_version}/)"""
@@ -459,20 +465,36 @@ def get_package_version() -> version.Version:
return version.Version(version_string)
+def get_release_branch_name(version_number: version.Version) -> str:
+ return f"release-v{version_number.major}.{version_number.minor}"
+
+
+def get_repo_and_check_clean_checkout() -> git.Repo:
+ """Get the project repo and check it's not got any uncommitted changes."""
+ try:
+ repo = git.Repo()
+ except git.InvalidGitRepositoryError:
+ raise click.ClickException("Not in Synapse repo.")
+ if repo.is_dirty():
+ raise click.ClickException("Uncommitted changes exist.")
+ return repo
+
+
def find_ref(repo: git.Repo, ref_name: str) -> Optional[git.HEAD]:
"""Find the branch/ref, looking first locally then in the remote."""
- if ref_name in repo.refs:
- return repo.refs[ref_name]
+ if ref_name in repo.references:
+ return repo.references[ref_name]
elif ref_name in repo.remote().refs:
return repo.remote().refs[ref_name]
else:
return None
-def update_branch(repo: git.Repo):
+def update_branch(repo: git.Repo) -> None:
"""Ensure branch is up to date if it has a remote"""
- if repo.active_branch.tracking_branch():
- repo.git.merge(repo.active_branch.tracking_branch().name)
+ tracking_branch = repo.active_branch.tracking_branch()
+ if tracking_branch:
+ repo.git.merge(tracking_branch.name)
def get_changes_for_version(wanted_version: version.Version) -> str:
@@ -536,7 +558,9 @@ def get_changes_for_version(wanted_version: version.Version) -> str:
return "\n".join(version_changelog)
-def generate_and_write_changelog(current_version: version.Version, new_version: str):
+def generate_and_write_changelog(
+ current_version: version.Version, new_version: str
+) -> None:
# We do this by getting a draft so that we can edit it before writing to the
# changelog.
result = run_until_successful(
@@ -558,8 +582,8 @@ def generate_and_write_changelog(current_version: version.Version, new_version:
f.write(existing_content)
# Remove all the news fragments
- for f in glob.iglob("changelog.d/*.*"):
- os.remove(f)
+ for filename in glob.iglob("changelog.d/*.*"):
+ os.remove(filename)
if __name__ == "__main__":
diff --git a/scripts-dev/sign_json.py b/scripts-dev/sign_json.py
index 9459543106..bb217799fb 100755
--- a/scripts-dev/sign_json.py
+++ b/scripts-dev/sign_json.py
@@ -27,7 +27,7 @@ from synapse.crypto.event_signing import add_hashes_and_signatures
from synapse.util import json_encoder
-def main():
+def main() -> None:
parser = argparse.ArgumentParser(
description="""Adds a signature to a JSON object.
diff --git a/stubs/sortedcontainers/sorteddict.pyi b/stubs/sortedcontainers/sorteddict.pyi
index e18d617281..7c399ab38d 100644
--- a/stubs/sortedcontainers/sorteddict.pyi
+++ b/stubs/sortedcontainers/sorteddict.pyi
@@ -85,12 +85,19 @@ class SortedDict(Dict[_KT, _VT]):
def popitem(self, index: int = ...) -> Tuple[_KT, _VT]: ...
def peekitem(self, index: int = ...) -> Tuple[_KT, _VT]: ...
def setdefault(self, key: _KT, default: Optional[_VT] = ...) -> _VT: ...
- @overload
- def update(self, __map: Mapping[_KT, _VT], **kwargs: _VT) -> None: ...
- @overload
- def update(self, __iterable: Iterable[Tuple[_KT, _VT]], **kwargs: _VT) -> None: ...
- @overload
- def update(self, **kwargs: _VT) -> None: ...
+ # Mypy now reports the first overload as an error, because typeshed widened the type
+ # of `__map` to its internal `_typeshed.SupportsKeysAndGetItem` type in
+ # https://github.com/python/typeshed/pull/6653
+ # Since sorteddicts don't change the signature of `update` from that of `dict`, we
+ # let the stubs for `update` inherit from the stubs for `dict`. (I suspect we could
+ # do the same for many othe methods.) We leave the stubs commented to better track
+ # how this file has evolved from the original stubs.
+ # @overload
+ # def update(self, __map: Mapping[_KT, _VT], **kwargs: _VT) -> None: ...
+ # @overload
+ # def update(self, __iterable: Iterable[Tuple[_KT, _VT]], **kwargs: _VT) -> None: ...
+ # @overload
+ # def update(self, **kwargs: _VT) -> None: ...
def __reduce__(
self,
) -> Tuple[
@@ -115,9 +122,7 @@ class SortedKeysView(KeysView[_KT_co], Sequence[_KT_co]):
def __getitem__(self, index: slice) -> List[_KT_co]: ...
def __delitem__(self, index: Union[int, slice]) -> None: ...
-class SortedItemsView( # type: ignore
- ItemsView[_KT_co, _VT_co], Sequence[Tuple[_KT_co, _VT_co]]
-):
+class SortedItemsView(ItemsView[_KT_co, _VT_co], Sequence[Tuple[_KT_co, _VT_co]]):
def __iter__(self) -> Iterator[Tuple[_KT_co, _VT_co]]: ...
@overload
def __getitem__(self, index: int) -> Tuple[_KT_co, _VT_co]: ...
diff --git a/synapse/api/auth.py b/synapse/api/auth.py
index 01c32417d8..931750668e 100644
--- a/synapse/api/auth.py
+++ b/synapse/api/auth.py
@@ -187,7 +187,7 @@ class Auth:
Once get_user_by_req has set up the opentracing span, this does the actual work.
"""
try:
- ip_addr = request.getClientIP()
+ ip_addr = request.getClientAddress().host
user_agent = get_request_user_agent(request)
access_token = self.get_access_token_from_request(request)
@@ -356,7 +356,7 @@ class Auth:
return None, None, None
if app_service.ip_range_whitelist:
- ip_address = IPAddress(request.getClientIP())
+ ip_address = IPAddress(request.getClientAddress().host)
if ip_address not in app_service.ip_range_whitelist:
return None, None, None
@@ -417,7 +417,8 @@ class Auth:
"""
if rights == "access":
- # first look in the database
+ # First look in the database to see if the access token is present
+ # as an opaque token.
r = await self.store.get_user_by_access_token(token)
if r:
valid_until_ms = r.valid_until_ms
@@ -434,7 +435,8 @@ class Auth:
return r
- # otherwise it needs to be a valid macaroon
+ # If the token isn't found in the database, then it could still be a
+ # macaroon, so we check that here.
try:
user_id, guest = self._parse_and_validate_macaroon(token, rights)
@@ -482,8 +484,12 @@ class Auth:
TypeError,
ValueError,
) as e:
- logger.warning("Invalid macaroon in auth: %s %s", type(e), e)
- raise InvalidClientTokenError("Invalid macaroon passed.")
+ logger.warning(
+ "Invalid access token in auth: %s %s.",
+ type(e),
+ e,
+ )
+ raise InvalidClientTokenError("Invalid access token passed.")
def _parse_and_validate_macaroon(
self, token: str, rights: str = "access"
@@ -504,10 +510,7 @@ class Auth:
try:
macaroon = pymacaroons.Macaroon.deserialize(token)
except Exception: # deserialize can throw more-or-less anything
- # doesn't look like a macaroon: treat it as an opaque token which
- # must be in the database.
- # TODO: it would be nice to get rid of this, but apparently some
- # people use access tokens which aren't macaroons
+ # The access token doesn't look like a macaroon.
raise _InvalidMacaroonException()
try:
diff --git a/synapse/api/constants.py b/synapse/api/constants.py
index 0172eb60b8..0ccd4c9558 100644
--- a/synapse/api/constants.py
+++ b/synapse/api/constants.py
@@ -255,7 +255,5 @@ class GuestAccess:
class ReceiptTypes:
READ: Final = "m.read"
-
-
-class ReadReceiptEventFields:
- MSC2285_HIDDEN: Final = "org.matrix.msc2285.hidden"
+ READ_PRIVATE: Final = "org.matrix.msc2285.read.private"
+ FULLY_READ: Final = "m.fully_read"
diff --git a/synapse/app/_base.py b/synapse/app/_base.py
index 37321f9133..3623c1724d 100644
--- a/synapse/app/_base.py
+++ b/synapse/app/_base.py
@@ -38,6 +38,7 @@ from typing import (
from cryptography.utils import CryptographyDeprecationWarning
from matrix_common.versionstring import get_distribution_version_string
+from typing_extensions import ParamSpec
import twisted
from twisted.internet import defer, error, reactor as _reactor
@@ -48,7 +49,6 @@ from twisted.logger import LoggingFile, LogLevel
from twisted.protocols.tls import TLSMemoryBIOFactory
from twisted.python.threadpool import ThreadPool
-import synapse
from synapse.api.constants import MAX_PDU_SIZE
from synapse.app import check_bind_error
from synapse.app.phone_stats_home import start_phone_stats_home
@@ -60,6 +60,7 @@ from synapse.events.spamcheck import load_legacy_spam_checkers
from synapse.events.third_party_rules import load_legacy_third_party_event_rules
from synapse.handlers.auth import load_legacy_password_auth_providers
from synapse.logging.context import PreserveLoggingContext
+from synapse.logging.opentracing import init_tracer
from synapse.metrics import install_gc_manager, register_threadpool
from synapse.metrics.background_process_metrics import wrap_as_background_process
from synapse.metrics.jemalloc import setup_jemalloc_stats
@@ -81,11 +82,12 @@ logger = logging.getLogger(__name__)
# list of tuples of function, args list, kwargs dict
_sighup_callbacks: List[
- Tuple[Callable[..., None], Tuple[Any, ...], Dict[str, Any]]
+ Tuple[Callable[..., None], Tuple[object, ...], Dict[str, object]]
] = []
+P = ParamSpec("P")
-def register_sighup(func: Callable[..., None], *args: Any, **kwargs: Any) -> None:
+def register_sighup(func: Callable[P, None], *args: P.args, **kwargs: P.kwargs) -> None:
"""
Register a function to be called when a SIGHUP occurs.
@@ -93,7 +95,9 @@ def register_sighup(func: Callable[..., None], *args: Any, **kwargs: Any) -> Non
func: Function to be called when sent a SIGHUP signal.
*args, **kwargs: args and kwargs to be passed to the target function.
"""
- _sighup_callbacks.append((func, args, kwargs))
+ # This type-ignore should be redundant once we use a mypy release with
+ # https://github.com/python/mypy/pull/12668.
+ _sighup_callbacks.append((func, args, kwargs)) # type: ignore[arg-type]
def start_worker_reactor(
@@ -214,7 +218,9 @@ def redirect_stdio_to_logs() -> None:
print("Redirected stdout/stderr to logs")
-def register_start(cb: Callable[..., Awaitable], *args: Any, **kwargs: Any) -> None:
+def register_start(
+ cb: Callable[P, Awaitable], *args: P.args, **kwargs: P.kwargs
+) -> None:
"""Register a callback with the reactor, to be called once it is running
This can be used to initialise parts of the system which require an asynchronous
@@ -431,7 +437,7 @@ async def start(hs: "HomeServer") -> None:
refresh_certificate(hs)
# Start the tracer
- synapse.logging.opentracing.init_tracer(hs) # type: ignore[attr-defined] # noqa
+ init_tracer(hs) # noqa
# Instantiate the modules so they can register their web resources to the module API
# before we start the listeners.
diff --git a/synapse/app/admin_cmd.py b/synapse/app/admin_cmd.py
index 2b0d92cbae..2a4c2e59cd 100644
--- a/synapse/app/admin_cmd.py
+++ b/synapse/app/admin_cmd.py
@@ -210,7 +210,7 @@ def start(config_options: List[str]) -> None:
config.logging.no_redirect_stdio = True
# Explicitly disable background processes
- config.server.update_user_directory = False
+ config.worker.should_update_user_directory = False
config.worker.run_background_tasks = False
config.worker.start_pushers = False
config.worker.pusher_shard_config.instances = []
diff --git a/synapse/app/generic_worker.py b/synapse/app/generic_worker.py
index 1865c671f4..2a9480a5c1 100644
--- a/synapse/app/generic_worker.py
+++ b/synapse/app/generic_worker.py
@@ -441,38 +441,6 @@ def start(config_options: List[str]) -> None:
"synapse.app.user_dir",
)
- if config.worker.worker_app == "synapse.app.appservice":
- if config.appservice.notify_appservices:
- sys.stderr.write(
- "\nThe appservices must be disabled in the main synapse process"
- "\nbefore they can be run in a separate worker."
- "\nPlease add ``notify_appservices: false`` to the main config"
- "\n"
- )
- sys.exit(1)
-
- # Force the appservice to start since they will be disabled in the main config
- config.appservice.notify_appservices = True
- else:
- # For other worker types we force this to off.
- config.appservice.notify_appservices = False
-
- if config.worker.worker_app == "synapse.app.user_dir":
- if config.server.update_user_directory:
- sys.stderr.write(
- "\nThe update_user_directory must be disabled in the main synapse process"
- "\nbefore they can be run in a separate worker."
- "\nPlease add ``update_user_directory: false`` to the main config"
- "\n"
- )
- sys.exit(1)
-
- # Force the pushers to start since they will be disabled in the main config
- config.server.update_user_directory = True
- else:
- # For other worker types we force this to off.
- config.server.update_user_directory = False
-
synapse.events.USE_FROZEN_DICTS = config.server.use_frozen_dicts
synapse.util.caches.TRACK_MEMORY_USAGE = config.caches.track_memory_usage
diff --git a/synapse/appservice/api.py b/synapse/appservice/api.py
index adc6b074da..d19f8dd996 100644
--- a/synapse/appservice/api.py
+++ b/synapse/appservice/api.py
@@ -17,6 +17,7 @@ import urllib.parse
from typing import TYPE_CHECKING, Dict, Iterable, List, Optional, Tuple
from prometheus_client import Counter
+from typing_extensions import TypeGuard
from synapse.api.constants import EventTypes, Membership, ThirdPartyEntityKind
from synapse.api.errors import CodeMessageException
@@ -66,7 +67,7 @@ def _is_valid_3pe_metadata(info: JsonDict) -> bool:
return True
-def _is_valid_3pe_result(r: JsonDict, field: str) -> bool:
+def _is_valid_3pe_result(r: object, field: str) -> TypeGuard[JsonDict]:
if not isinstance(r, dict):
return False
diff --git a/synapse/config/appservice.py b/synapse/config/appservice.py
index 720b90a283..24498e7944 100644
--- a/synapse/config/appservice.py
+++ b/synapse/config/appservice.py
@@ -33,7 +33,6 @@ class AppServiceConfig(Config):
def read_config(self, config: JsonDict, **kwargs: Any) -> None:
self.app_service_config_files = config.get("app_service_config_files", [])
- self.notify_appservices = config.get("notify_appservices", True)
self.track_appservice_user_ips = config.get("track_appservice_user_ips", False)
def generate_config_section(cls, **kwargs: Any) -> str:
@@ -56,7 +55,8 @@ def load_appservices(
) -> List[ApplicationService]:
"""Returns a list of Application Services from the config files."""
if not isinstance(config_files, list):
- logger.warning("Expected %s to be a list of AS config files.", config_files)
+ # type-ignore: this function gets arbitrary json value; we do use this path.
+ logger.warning("Expected %s to be a list of AS config files.", config_files) # type: ignore[unreachable]
return []
# Dicts of value -> filename
diff --git a/synapse/config/experimental.py b/synapse/config/experimental.py
index 421ed7481b..b20d949689 100644
--- a/synapse/config/experimental.py
+++ b/synapse/config/experimental.py
@@ -32,7 +32,7 @@ class ExperimentalConfig(Config):
# MSC2716 (importing historical messages)
self.msc2716_enabled: bool = experimental.get("msc2716_enabled", False)
- # MSC2285 (hidden read receipts)
+ # MSC2285 (private read receipts)
self.msc2285_enabled: bool = experimental.get("msc2285_enabled", False)
# MSC3244 (room version capabilities)
@@ -81,3 +81,6 @@ class ExperimentalConfig(Config):
# MSC2815 (allow room moderators to view redacted event content)
self.msc2815_enabled: bool = experimental.get("msc2815_enabled", False)
+
+ # MSC3786 (Add a default push rule to ignore m.room.server_acl events)
+ self.msc3786_enabled: bool = experimental.get("msc3786_enabled", False)
diff --git a/synapse/config/federation.py b/synapse/config/federation.py
index 0e74f70784..f83f93c0ef 100644
--- a/synapse/config/federation.py
+++ b/synapse/config/federation.py
@@ -46,7 +46,7 @@ class FederationConfig(Config):
)
self.allow_device_name_lookup_over_federation = config.get(
- "allow_device_name_lookup_over_federation", True
+ "allow_device_name_lookup_over_federation", False
)
def generate_config_section(self, **kwargs: Any) -> str:
@@ -81,11 +81,11 @@ class FederationConfig(Config):
#
#allow_profile_lookup_over_federation: false
- # Uncomment to disable device display name lookup over federation. By default, the
- # Federation API allows other homeservers to obtain device display names of any user
- # on this homeserver. Defaults to 'true'.
+ # Uncomment to allow device display name lookup over federation. By default, the
+ # Federation API prevents other homeservers from obtaining the display names of
+ # user devices on this homeserver. Defaults to 'false'.
#
- #allow_device_name_lookup_over_federation: false
+ #allow_device_name_lookup_over_federation: true
"""
diff --git a/synapse/config/logger.py b/synapse/config/logger.py
index 99db9e1e39..470b8b4492 100644
--- a/synapse/config/logger.py
+++ b/synapse/config/logger.py
@@ -110,13 +110,6 @@ loggers:
# information such as access tokens.
level: INFO
- twisted:
- # We send the twisted logging directly to the file handler,
- # to work around https://github.com/matrix-org/synapse/issues/3471
- # when using "buffer" logger. Use "console" to log to stderr instead.
- handlers: [file]
- propagate: false
-
root:
level: INFO
diff --git a/synapse/config/registration.py b/synapse/config/registration.py
index 39e9acb62a..d2d0425e62 100644
--- a/synapse/config/registration.py
+++ b/synapse/config/registration.py
@@ -43,6 +43,9 @@ class RegistrationConfig(Config):
self.registration_requires_token = config.get(
"registration_requires_token", False
)
+ self.enable_registration_token_3pid_bypass = config.get(
+ "enable_registration_token_3pid_bypass", False
+ )
self.registration_shared_secret = config.get("registration_shared_secret")
self.bcrypt_rounds = config.get("bcrypt_rounds", 12)
@@ -309,6 +312,12 @@ class RegistrationConfig(Config):
#
#registration_requires_token: true
+ # Allow users to submit a token during registration to bypass any required 3pid
+ # steps configured in `registrations_require_3pid`.
+ # Defaults to false, requiring that registration tokens (if enabled) complete a 3pid flow.
+ #
+ #enable_registration_token_3pid_bypass: false
+
# If set, allows registration of standard or admin accounts by anyone who
# has the shared secret, even if registration is otherwise disabled.
#
diff --git a/synapse/config/server.py b/synapse/config/server.py
index d771045b52..005a3ee48c 100644
--- a/synapse/config/server.py
+++ b/synapse/config/server.py
@@ -186,7 +186,7 @@ KNOWN_RESOURCES = {
class HttpResourceConfig:
names: List[str] = attr.ib(
factory=list,
- validator=attr.validators.deep_iterable(attr.validators.in_(KNOWN_RESOURCES)), # type: ignore
+ validator=attr.validators.deep_iterable(attr.validators.in_(KNOWN_RESOURCES)),
)
compress: bool = attr.ib(
default=False,
@@ -231,9 +231,7 @@ class ManholeConfig:
class LimitRemoteRoomsConfig:
enabled: bool = attr.ib(validator=attr.validators.instance_of(bool), default=False)
complexity: Union[float, int] = attr.ib(
- validator=attr.validators.instance_of(
- (float, int) # type: ignore[arg-type] # noqa
- ),
+ validator=attr.validators.instance_of((float, int)), # noqa
default=1.0,
)
complexity_error: str = attr.ib(
@@ -321,10 +319,6 @@ class ServerConfig(Config):
self.presence_router_config,
) = load_module(presence_router_config, ("presence", "presence_router"))
- # Whether to update the user directory or not. This should be set to
- # false only if we are updating the user directory in a worker
- self.update_user_directory = config.get("update_user_directory", True)
-
# whether to enable the media repository endpoints. This should be set
# to false if the media repository is running as a separate endpoint;
# doing so ensures that we will not run cache cleanup jobs on the
@@ -415,6 +409,7 @@ class ServerConfig(Config):
)
self.mau_trial_days = config.get("mau_trial_days", 0)
+ self.mau_appservice_trial_days = config.get("mau_appservice_trial_days", {})
self.mau_limit_alerting = config.get("mau_limit_alerting", True)
# How long to keep redacted events in the database in unredacted form
@@ -1107,6 +1102,11 @@ class ServerConfig(Config):
# sign up in a short space of time never to return after their initial
# session.
#
+ # The option `mau_appservice_trial_days` is similar to `mau_trial_days`, but
+ # applies a different trial number if the user was registered by an appservice.
+ # A value of 0 means no trial days are applied. Appservices not listed in this
+ # dictionary use the value of `mau_trial_days` instead.
+ #
# 'mau_limit_alerting' is a means of limiting client side alerting
# should the mau limit be reached. This is useful for small instances
# where the admin has 5 mau seats (say) for 5 specific people and no
@@ -1117,6 +1117,8 @@ class ServerConfig(Config):
#max_mau_value: 50
#mau_trial_days: 2
#mau_limit_alerting: false
+ #mau_appservice_trial_days:
+ # "appservice-id": 1
# If enabled, the metrics for the number of monthly active users will
# be populated, however no one will be limited. If limit_usage_by_mau
diff --git a/synapse/config/workers.py b/synapse/config/workers.py
index a5479dfca9..e1569b3c14 100644
--- a/synapse/config/workers.py
+++ b/synapse/config/workers.py
@@ -14,7 +14,8 @@
# limitations under the License.
import argparse
-from typing import Any, List, Union
+import logging
+from typing import Any, Dict, List, Union
import attr
@@ -42,6 +43,13 @@ synapse process before they can be run in a separate worker.
Please add ``start_pushers: false`` to the main config
"""
+_DEPRECATED_WORKER_DUTY_OPTION_USED = """
+The '%s' configuration option is deprecated and will be removed in a future
+Synapse version. Please use ``%s: name_of_worker`` instead.
+"""
+
+logger = logging.getLogger(__name__)
+
def _instance_to_list_converter(obj: Union[str, List[str]]) -> List[str]:
"""Helper for allowing parsing a string or list of strings to a config
@@ -296,6 +304,112 @@ class WorkerConfig(Config):
self.worker_name is None and background_tasks_instance == "master"
) or self.worker_name == background_tasks_instance
+ self.should_notify_appservices = self._should_this_worker_perform_duty(
+ config,
+ legacy_master_option_name="notify_appservices",
+ legacy_worker_app_name="synapse.app.appservice",
+ new_option_name="notify_appservices_from_worker",
+ )
+
+ self.should_update_user_directory = self._should_this_worker_perform_duty(
+ config,
+ legacy_master_option_name="update_user_directory",
+ legacy_worker_app_name="synapse.app.user_dir",
+ new_option_name="update_user_directory_from_worker",
+ )
+
+ def _should_this_worker_perform_duty(
+ self,
+ config: Dict[str, Any],
+ legacy_master_option_name: str,
+ legacy_worker_app_name: str,
+ new_option_name: str,
+ ) -> bool:
+ """
+ Figures out whether this worker should perform a certain duty.
+
+ This function is temporary and is only to deal with the complexity
+ of allowing old, transitional and new configurations all at once.
+
+ Contradictions between the legacy and new part of a transitional configuration
+ will lead to a ConfigError.
+
+ Parameters:
+ config: The config dictionary
+ legacy_master_option_name: The name of a legacy option, whose value is boolean,
+ specifying whether it's the master that should handle a certain duty.
+ e.g. "notify_appservices"
+ legacy_worker_app_name: The name of a legacy Synapse worker application
+ that would traditionally perform this duty.
+ e.g. "synapse.app.appservice"
+ new_option_name: The name of the new option, whose value is the name of a
+ designated worker to perform the duty.
+ e.g. "notify_appservices_from_worker"
+ """
+
+ # None means 'unspecified'; True means 'run here' and False means
+ # 'don't run here'.
+ new_option_should_run_here = None
+ if new_option_name in config:
+ designated_worker = config[new_option_name] or "master"
+ new_option_should_run_here = (
+ designated_worker == "master" and self.worker_name is None
+ ) or designated_worker == self.worker_name
+
+ legacy_option_should_run_here = None
+ if legacy_master_option_name in config:
+ run_on_master = bool(config[legacy_master_option_name])
+
+ legacy_option_should_run_here = (
+ self.worker_name is None and run_on_master
+ ) or (self.worker_app == legacy_worker_app_name and not run_on_master)
+
+ # Suggest using the new option instead.
+ logger.warning(
+ _DEPRECATED_WORKER_DUTY_OPTION_USED,
+ legacy_master_option_name,
+ new_option_name,
+ )
+
+ if self.worker_app == legacy_worker_app_name and config.get(
+ legacy_master_option_name, True
+ ):
+ # As an extra bit of complication, we need to check that the
+ # specialised worker is only used if the legacy config says the
+ # master isn't performing the duties.
+ raise ConfigError(
+ f"Cannot use deprecated worker app type '{legacy_worker_app_name}' whilst deprecated option '{legacy_master_option_name}' is not set to false.\n"
+ f"Consider setting `worker_app: synapse.app.generic_worker` and using the '{new_option_name}' option instead.\n"
+ f"The '{new_option_name}' option replaces '{legacy_master_option_name}'."
+ )
+
+ if new_option_should_run_here is None and legacy_option_should_run_here is None:
+ # Neither option specified; the fallback behaviour is to run on the main process
+ return self.worker_name is None
+
+ if (
+ new_option_should_run_here is not None
+ and legacy_option_should_run_here is not None
+ ):
+ # Both options specified; ensure they match!
+ if new_option_should_run_here != legacy_option_should_run_here:
+ update_worker_type = (
+ " and set worker_app: synapse.app.generic_worker"
+ if self.worker_app == legacy_worker_app_name
+ else ""
+ )
+ # If the values conflict, we suggest the admin removes the legacy option
+ # for simplicity.
+ raise ConfigError(
+ f"Conflicting configuration options: {legacy_master_option_name} (legacy), {new_option_name} (new).\n"
+ f"Suggestion: remove {legacy_master_option_name}{update_worker_type}.\n"
+ )
+
+ # We've already validated that these aren't conflicting; now just see if
+ # either is True.
+ # (By this point, these are either the same value or only one is not None.)
+ return bool(new_option_should_run_here or legacy_option_should_run_here)
+
def generate_config_section(self, **kwargs: Any) -> str:
return """\
## Workers ##
diff --git a/synapse/events/__init__.py b/synapse/events/__init__.py
index 9acb3c0cc4..c238376caf 100644
--- a/synapse/events/__init__.py
+++ b/synapse/events/__init__.py
@@ -213,10 +213,17 @@ class _EventInternalMetadata:
return self.outlier
def is_out_of_band_membership(self) -> bool:
- """Whether this is an out of band membership, like an invite or an invite
- rejection. This is needed as those events are marked as outliers, but
- they still need to be processed as if they're new events (e.g. updating
- invite state in the database, relaying to clients, etc).
+ """Whether this event is an out-of-band membership.
+
+ OOB memberships are a special case of outlier events: they are membership events
+ for federated rooms that we aren't full members of. Examples include invites
+ received over federation, and rejections for such invites.
+
+ The concept of an OOB membership is needed because these events need to be
+ processed as if they're new regular events (e.g. updating membership state in
+ the database, relaying to clients via /sync, etc) despite being outliers.
+
+ See also https://matrix-org.github.io/synapse/develop/development/room-dag-concepts.html#out-of-band-membership-events.
(Added in synapse 0.99.0, so may be unreliable for events received before that)
"""
diff --git a/synapse/events/presence_router.py b/synapse/events/presence_router.py
index a58f313e8b..bb4a6bd957 100644
--- a/synapse/events/presence_router.py
+++ b/synapse/events/presence_router.py
@@ -22,11 +22,16 @@ from typing import (
List,
Optional,
Set,
+ TypeVar,
Union,
)
+from typing_extensions import ParamSpec
+
+from twisted.internet.defer import CancelledError
+
from synapse.api.presence import UserPresenceState
-from synapse.util.async_helpers import maybe_awaitable
+from synapse.util.async_helpers import delay_cancellation, maybe_awaitable
if TYPE_CHECKING:
from synapse.server import HomeServer
@@ -40,6 +45,10 @@ GET_INTERESTED_USERS_CALLBACK = Callable[[str], Awaitable[Union[Set[str], str]]]
logger = logging.getLogger(__name__)
+P = ParamSpec("P")
+R = TypeVar("R")
+
+
def load_legacy_presence_router(hs: "HomeServer") -> None:
"""Wrapper that loads a presence router module configured using the old
configuration, and registers the hooks they implement.
@@ -63,13 +72,15 @@ def load_legacy_presence_router(hs: "HomeServer") -> None:
# All methods that the module provides should be async, but this wasn't enforced
# in the old module system, so we wrap them if needed
- def async_wrapper(f: Optional[Callable]) -> Optional[Callable[..., Awaitable]]:
+ def async_wrapper(
+ f: Optional[Callable[P, R]]
+ ) -> Optional[Callable[P, Awaitable[R]]]:
# f might be None if the callback isn't implemented by the module. In this
# case we don't want to register a callback at all so we return None.
if f is None:
return None
- def run(*args: Any, **kwargs: Any) -> Awaitable:
+ def run(*args: P.args, **kwargs: P.kwargs) -> Awaitable[R]:
# Assertion required because mypy can't prove we won't change `f`
# back to `None`. See
# https://mypy.readthedocs.io/en/latest/common_issues.html#narrowing-and-inner-functions
@@ -80,7 +91,7 @@ def load_legacy_presence_router(hs: "HomeServer") -> None:
return run
# Register the hooks through the module API.
- hooks = {
+ hooks: Dict[str, Optional[Callable[..., Any]]] = {
hook: async_wrapper(getattr(presence_router, hook, None))
for hook in presence_router_methods
}
@@ -147,7 +158,11 @@ class PresenceRouter:
# run all the callbacks for get_users_for_states and combine the results
for callback in self._get_users_for_states_callbacks:
try:
- result = await callback(state_updates)
+ # Note: result is an object here, because we don't trust modules to
+ # return the types they're supposed to.
+ result: object = await delay_cancellation(callback(state_updates))
+ except CancelledError:
+ raise
except Exception as e:
logger.warning("Failed to run module API callback %s: %s", callback, e)
continue
@@ -199,7 +214,9 @@ class PresenceRouter:
# run all the callbacks for get_interested_users and combine the results
for callback in self._get_interested_users_callbacks:
try:
- result = await callback(user_id)
+ result = await delay_cancellation(callback(user_id))
+ except CancelledError:
+ raise
except Exception as e:
logger.warning("Failed to run module API callback %s: %s", callback, e)
continue
diff --git a/synapse/events/spamcheck.py b/synapse/events/spamcheck.py
index cd80fcf9d1..3b6795d40f 100644
--- a/synapse/events/spamcheck.py
+++ b/synapse/events/spamcheck.py
@@ -31,7 +31,7 @@ from synapse.rest.media.v1._base import FileInfo
from synapse.rest.media.v1.media_storage import ReadableFileWrapper
from synapse.spam_checker_api import RegistrationBehaviour
from synapse.types import RoomAlias, UserProfile
-from synapse.util.async_helpers import maybe_awaitable
+from synapse.util.async_helpers import delay_cancellation, maybe_awaitable
if TYPE_CHECKING:
import synapse.events
@@ -255,7 +255,7 @@ class SpamChecker:
will be used as the error message returned to the user.
"""
for callback in self._check_event_for_spam_callbacks:
- res: Union[bool, str] = await callback(event)
+ res: Union[bool, str] = await delay_cancellation(callback(event))
if res:
return res
@@ -276,7 +276,10 @@ class SpamChecker:
Whether the user may join the room
"""
for callback in self._user_may_join_room_callbacks:
- if await callback(user_id, room_id, is_invited) is False:
+ may_join_room = await delay_cancellation(
+ callback(user_id, room_id, is_invited)
+ )
+ if may_join_room is False:
return False
return True
@@ -297,7 +300,10 @@ class SpamChecker:
True if the user may send an invite, otherwise False
"""
for callback in self._user_may_invite_callbacks:
- if await callback(inviter_userid, invitee_userid, room_id) is False:
+ may_invite = await delay_cancellation(
+ callback(inviter_userid, invitee_userid, room_id)
+ )
+ if may_invite is False:
return False
return True
@@ -322,7 +328,10 @@ class SpamChecker:
True if the user may send the invite, otherwise False
"""
for callback in self._user_may_send_3pid_invite_callbacks:
- if await callback(inviter_userid, medium, address, room_id) is False:
+ may_send_3pid_invite = await delay_cancellation(
+ callback(inviter_userid, medium, address, room_id)
+ )
+ if may_send_3pid_invite is False:
return False
return True
@@ -339,7 +348,8 @@ class SpamChecker:
True if the user may create a room, otherwise False
"""
for callback in self._user_may_create_room_callbacks:
- if await callback(userid) is False:
+ may_create_room = await delay_cancellation(callback(userid))
+ if may_create_room is False:
return False
return True
@@ -359,7 +369,10 @@ class SpamChecker:
True if the user may create a room alias, otherwise False
"""
for callback in self._user_may_create_room_alias_callbacks:
- if await callback(userid, room_alias) is False:
+ may_create_room_alias = await delay_cancellation(
+ callback(userid, room_alias)
+ )
+ if may_create_room_alias is False:
return False
return True
@@ -377,7 +390,8 @@ class SpamChecker:
True if the user may publish the room, otherwise False
"""
for callback in self._user_may_publish_room_callbacks:
- if await callback(userid, room_id) is False:
+ may_publish_room = await delay_cancellation(callback(userid, room_id))
+ if may_publish_room is False:
return False
return True
@@ -400,7 +414,7 @@ class SpamChecker:
for callback in self._check_username_for_spam_callbacks:
# Make a copy of the user profile object to ensure the spam checker cannot
# modify it.
- if await callback(user_profile.copy()):
+ if await delay_cancellation(callback(user_profile.copy())):
return True
return False
@@ -428,7 +442,7 @@ class SpamChecker:
"""
for callback in self._check_registration_for_spam_callbacks:
- behaviour = await (
+ behaviour = await delay_cancellation(
callback(email_threepid, username, request_info, auth_provider_id)
)
assert isinstance(behaviour, RegistrationBehaviour)
@@ -472,7 +486,7 @@ class SpamChecker:
"""
for callback in self._check_media_file_for_spam_callbacks:
- spam = await callback(file_wrapper, file_info)
+ spam = await delay_cancellation(callback(file_wrapper, file_info))
if spam:
return True
diff --git a/synapse/events/third_party_rules.py b/synapse/events/third_party_rules.py
index ef68e20282..9f4ff9799c 100644
--- a/synapse/events/third_party_rules.py
+++ b/synapse/events/third_party_rules.py
@@ -14,12 +14,14 @@
import logging
from typing import TYPE_CHECKING, Any, Awaitable, Callable, List, Optional, Tuple
+from twisted.internet.defer import CancelledError
+
from synapse.api.errors import ModuleFailedException, SynapseError
from synapse.events import EventBase
from synapse.events.snapshot import EventContext
from synapse.storage.roommember import ProfileInfo
from synapse.types import Requester, StateMap
-from synapse.util.async_helpers import maybe_awaitable
+from synapse.util.async_helpers import delay_cancellation, maybe_awaitable
if TYPE_CHECKING:
from synapse.server import HomeServer
@@ -263,7 +265,11 @@ class ThirdPartyEventRules:
for callback in self._check_event_allowed_callbacks:
try:
- res, replacement_data = await callback(event, state_events)
+ res, replacement_data = await delay_cancellation(
+ callback(event, state_events)
+ )
+ except CancelledError:
+ raise
except SynapseError as e:
# FIXME: Being able to throw SynapseErrors is relied upon by
# some modules. PR #10386 accidentally broke this ability.
@@ -333,8 +339,13 @@ class ThirdPartyEventRules:
for callback in self._check_threepid_can_be_invited_callbacks:
try:
- if await callback(medium, address, state_events) is False:
+ threepid_can_be_invited = await delay_cancellation(
+ callback(medium, address, state_events)
+ )
+ if threepid_can_be_invited is False:
return False
+ except CancelledError:
+ raise
except Exception as e:
logger.warning("Failed to run module API callback %s: %s", callback, e)
@@ -361,8 +372,13 @@ class ThirdPartyEventRules:
for callback in self._check_visibility_can_be_modified_callbacks:
try:
- if await callback(room_id, state_events, new_visibility) is False:
+ visibility_can_be_modified = await delay_cancellation(
+ callback(room_id, state_events, new_visibility)
+ )
+ if visibility_can_be_modified is False:
return False
+ except CancelledError:
+ raise
except Exception as e:
logger.warning("Failed to run module API callback %s: %s", callback, e)
@@ -400,8 +416,11 @@ class ThirdPartyEventRules:
"""
for callback in self._check_can_shutdown_room_callbacks:
try:
- if await callback(user_id, room_id) is False:
+ can_shutdown_room = await delay_cancellation(callback(user_id, room_id))
+ if can_shutdown_room is False:
return False
+ except CancelledError:
+ raise
except Exception as e:
logger.exception(
"Failed to run module API callback %s: %s", callback, e
@@ -422,8 +441,13 @@ class ThirdPartyEventRules:
"""
for callback in self._check_can_deactivate_user_callbacks:
try:
- if await callback(user_id, by_admin) is False:
+ can_deactivate_user = await delay_cancellation(
+ callback(user_id, by_admin)
+ )
+ if can_deactivate_user is False:
return False
+ except CancelledError:
+ raise
except Exception as e:
logger.exception(
"Failed to run module API callback %s: %s", callback, e
diff --git a/synapse/events/utils.py b/synapse/events/utils.py
index f8d3ba5456..ac91c5eb57 100644
--- a/synapse/events/utils.py
+++ b/synapse/events/utils.py
@@ -22,12 +22,12 @@ from typing import (
Iterable,
List,
Mapping,
+ MutableMapping,
Optional,
Union,
)
import attr
-from frozendict import frozendict
from synapse.api.constants import EventContentFields, EventTypes, RelationTypes
from synapse.api.errors import Codes, SynapseError
@@ -204,7 +204,9 @@ def _copy_field(src: JsonDict, dst: JsonDict, field: List[str]) -> None:
key_to_move = field.pop(-1)
sub_dict = src
for sub_field in field: # e.g. sub_field => "content"
- if sub_field in sub_dict and type(sub_dict[sub_field]) in [dict, frozendict]:
+ if sub_field in sub_dict and isinstance(
+ sub_dict[sub_field], collections.abc.Mapping
+ ):
sub_dict = sub_dict[sub_field]
else:
return
@@ -425,13 +427,12 @@ class EventClientSerializer:
# Check if there are any bundled aggregations to include with the event.
if bundle_aggregations:
- event_aggregations = bundle_aggregations.get(event.event_id)
- if event_aggregations:
+ if event.event_id in bundle_aggregations:
self._inject_bundled_aggregations(
event,
time_now,
config,
- event_aggregations,
+ bundle_aggregations,
serialized_event,
apply_edits=apply_edits,
)
@@ -470,7 +471,7 @@ class EventClientSerializer:
event: EventBase,
time_now: int,
config: SerializeEventConfig,
- aggregations: "BundledAggregations",
+ bundled_aggregations: Dict[str, "BundledAggregations"],
serialized_event: JsonDict,
apply_edits: bool,
) -> None:
@@ -480,22 +481,37 @@ class EventClientSerializer:
event: The event being serialized.
time_now: The current time in milliseconds
config: Event serialization config
- aggregations: The bundled aggregation to serialize.
+ bundled_aggregations: Bundled aggregations to be injected.
+ A map from event_id to aggregation data. Must contain at least an
+ entry for `event`.
+
+ While serializing the bundled aggregations this map may be searched
+ again for additional events in a recursive manner.
serialized_event: The serialized event which may be modified.
apply_edits: Whether the content of the event should be modified to reflect
any replacement in `aggregations.replace`.
"""
+
+ # We have already checked that aggregations exist for this event.
+ event_aggregations = bundled_aggregations[event.event_id]
+
+ # The JSON dictionary to be added under the unsigned property of the event
+ # being serialized.
serialized_aggregations = {}
- if aggregations.annotations:
- serialized_aggregations[RelationTypes.ANNOTATION] = aggregations.annotations
+ if event_aggregations.annotations:
+ serialized_aggregations[
+ RelationTypes.ANNOTATION
+ ] = event_aggregations.annotations
- if aggregations.references:
- serialized_aggregations[RelationTypes.REFERENCE] = aggregations.references
+ if event_aggregations.references:
+ serialized_aggregations[
+ RelationTypes.REFERENCE
+ ] = event_aggregations.references
- if aggregations.replace:
+ if event_aggregations.replace:
# If there is an edit, optionally apply it to the event.
- edit = aggregations.replace
+ edit = event_aggregations.replace
if apply_edits:
self._apply_edit(event, serialized_event, edit)
@@ -506,19 +522,16 @@ class EventClientSerializer:
"sender": edit.sender,
}
- # If this event is the start of a thread, include a summary of the replies.
- if aggregations.thread:
- thread = aggregations.thread
+ # Include any threaded replies to this event.
+ if event_aggregations.thread:
+ thread = event_aggregations.thread
- # Don't bundle aggregations as this could recurse forever.
- serialized_latest_event = serialize_event(
- thread.latest_event, time_now, config=config
+ serialized_latest_event = self.serialize_event(
+ thread.latest_event,
+ time_now,
+ config=config,
+ bundle_aggregations=bundled_aggregations,
)
- # Manually apply an edit, if one exists.
- if thread.latest_edit:
- self._apply_edit(
- thread.latest_event, serialized_latest_event, thread.latest_edit
- )
thread_summary = {
"latest_event": serialized_latest_event,
@@ -568,10 +581,20 @@ class EventClientSerializer:
]
-def copy_power_levels_contents(
- old_power_levels: Mapping[str, Union[int, Mapping[str, int]]]
+_PowerLevel = Union[str, int]
+
+
+def copy_and_fixup_power_levels_contents(
+ old_power_levels: Mapping[str, Union[_PowerLevel, Mapping[str, _PowerLevel]]]
) -> Dict[str, Union[int, Dict[str, int]]]:
- """Copy the content of a power_levels event, unfreezing frozendicts along the way
+ """Copy the content of a power_levels event, unfreezing frozendicts along the way.
+
+ We accept as input power level values which are strings, provided they represent an
+ integer, e.g. `"`100"` instead of 100. Such strings are converted to integers
+ in the returned dictionary (hence "fixup" in the function name).
+
+ Note that future room versions will outlaw such stringy power levels (see
+ https://github.com/matrix-org/matrix-spec/issues/853).
Raises:
TypeError if the input does not look like a valid power levels event content
@@ -580,29 +603,47 @@ def copy_power_levels_contents(
raise TypeError("Not a valid power-levels content: %r" % (old_power_levels,))
power_levels: Dict[str, Union[int, Dict[str, int]]] = {}
+
for k, v in old_power_levels.items():
-
- if isinstance(v, int):
- power_levels[k] = v
- continue
-
if isinstance(v, collections.abc.Mapping):
h: Dict[str, int] = {}
power_levels[k] = h
for k1, v1 in v.items():
- # we should only have one level of nesting
- if not isinstance(v1, int):
- raise TypeError(
- "Invalid power_levels value for %s.%s: %r" % (k, k1, v1)
- )
- h[k1] = v1
- continue
+ _copy_power_level_value_as_integer(v1, h, k1)
- raise TypeError("Invalid power_levels value for %s: %r" % (k, v))
+ else:
+ _copy_power_level_value_as_integer(v, power_levels, k)
return power_levels
+def _copy_power_level_value_as_integer(
+ old_value: object,
+ power_levels: MutableMapping[str, Any],
+ key: str,
+) -> None:
+ """Set `power_levels[key]` to the integer represented by `old_value`.
+
+ :raises TypeError: if `old_value` is not an integer, nor a base-10 string
+ representation of an integer.
+ """
+ if isinstance(old_value, int):
+ power_levels[key] = old_value
+ return
+
+ if isinstance(old_value, str):
+ try:
+ parsed_value = int(old_value, base=10)
+ except ValueError:
+ # Fall through to the final TypeError.
+ pass
+ else:
+ power_levels[key] = parsed_value
+ return
+
+ raise TypeError(f"Invalid power_levels value for {key}: {old_value}")
+
+
def validate_canonicaljson(value: Any) -> None:
"""
Ensure that the JSON object is valid according to the rules of canonical JSON.
@@ -622,7 +663,7 @@ def validate_canonicaljson(value: Any) -> None:
# Note that Infinity, -Infinity, and NaN are also considered floats.
raise SynapseError(400, "Bad JSON value: float", Codes.BAD_JSON)
- elif isinstance(value, (dict, frozendict)):
+ elif isinstance(value, collections.abc.Mapping):
for v in value.values():
validate_canonicaljson(v)
diff --git a/synapse/federation/federation_client.py b/synapse/federation/federation_client.py
index 6a59cb4b71..17eff60909 100644
--- a/synapse/federation/federation_client.py
+++ b/synapse/federation/federation_client.py
@@ -618,7 +618,7 @@ class FederationClient(FederationBase):
#
# Dendrite returns a 404 (with a body of "404 page not found");
# Conduit returns a 404 (with no body); and Synapse returns a 400
- # with M_UNRECOGNISED.
+ # with M_UNRECOGNIZED.
#
# This needs to be rather specific as some endpoints truly do return 404
# errors.
@@ -1426,6 +1426,8 @@ class FederationClient(FederationBase):
room = res.get("room")
if not isinstance(room, dict):
raise InvalidResponseError("'room' must be a dict")
+ if room.get("room_id") != room_id:
+ raise InvalidResponseError("wrong room returned in hierarchy response")
# Validate children_state of the room.
children_state = room.pop("children_state", [])
diff --git a/synapse/federation/federation_server.py b/synapse/federation/federation_server.py
index beab1227b8..884b5d60b4 100644
--- a/synapse/federation/federation_server.py
+++ b/synapse/federation/federation_server.py
@@ -268,8 +268,8 @@ class FederationServer(FederationBase):
transaction_id=transaction_id,
destination=destination,
origin=origin,
- origin_server_ts=transaction_data.get("origin_server_ts"), # type: ignore
- pdus=transaction_data.get("pdus"), # type: ignore
+ origin_server_ts=transaction_data.get("origin_server_ts"), # type: ignore[arg-type]
+ pdus=transaction_data.get("pdus"),
edus=transaction_data.get("edus"),
)
diff --git a/synapse/federation/sender/__init__.py b/synapse/federation/sender/__init__.py
index 30e2421efc..6d2f46318b 100644
--- a/synapse/federation/sender/__init__.py
+++ b/synapse/federation/sender/__init__.py
@@ -343,9 +343,16 @@ class FederationSender(AbstractFederationSender):
last_token, self._last_poked_id, limit=100
)
- logger.debug("Handling %s -> %s", last_token, next_token)
+ logger.debug(
+ "Handling %i -> %i: %i events to send (current id %i)",
+ last_token,
+ next_token,
+ len(events),
+ self._last_poked_id,
+ )
if not events and next_token >= self._last_poked_id:
+ logger.debug("All events processed")
break
async def handle_event(event: EventBase) -> None:
@@ -353,9 +360,53 @@ class FederationSender(AbstractFederationSender):
send_on_behalf_of = event.internal_metadata.get_send_on_behalf_of()
is_mine = self.is_mine_id(event.sender)
if not is_mine and send_on_behalf_of is None:
+ logger.debug("Not sending remote-origin event %s", event)
return
+ # We also want to not send out-of-band membership events.
+ #
+ # OOB memberships are used in three (and a half) situations:
+ #
+ # (1) invite events which we have received over federation. Those
+ # will have a `sender` on a different server, so will be
+ # skipped by the "is_mine" test above anyway.
+ #
+ # (2) rejections of invites to federated rooms - either remotely
+ # or locally generated. (Such rejections are normally
+ # created via federation, in which case the remote server is
+ # responsible for sending out the rejection. If that fails,
+ # we'll create a leave event locally, but that's only really
+ # for the benefit of the invited user - we don't have enough
+ # information to send it out over federation).
+ #
+ # (2a) rescinded knocks. These are identical to rejected invites.
+ #
+ # (3) knock events which we have sent over federation. As with
+ # invite rejections, the remote server should send them out to
+ # the federation.
+ #
+ # So, in all the above cases, we want to ignore such events.
+ #
+ # OOB memberships are always(?) outliers anyway, so if we *don't*
+ # ignore them, we'll get an exception further down when we try to
+ # fetch the membership list for the room.
+ #
+ # Arguably, we could equivalently ignore all outliers here, since
+ # in theory the only way for an outlier with a local `sender` to
+ # exist is by being an OOB membership (via one of (2), (2a) or (3)
+ # above).
+ #
+ if event.internal_metadata.is_out_of_band_membership():
+ logger.debug("Not sending OOB membership event %s", event)
+ return
+
+ # Finally, there are some other events that we should not send out
+ # until someone asks for them. They are explicitly flagged as such
+ # with `proactively_send: False`.
if not event.internal_metadata.should_proactively_send():
+ logger.debug(
+ "Not sending event with proactively_send=false: %s", event
+ )
return
destinations: Optional[Set[str]] = None
@@ -419,7 +470,10 @@ class FederationSender(AbstractFederationSender):
"federation_sender"
).observe((now - ts) / 1000)
- async def handle_room_events(events: Iterable[EventBase]) -> None:
+ async def handle_room_events(events: List[EventBase]) -> None:
+ logger.debug(
+ "Handling %i events in room %s", len(events), events[0].room_id
+ )
with Measure(self.clock, "handle_room_events"):
for event in events:
await handle_event(event)
@@ -438,6 +492,7 @@ class FederationSender(AbstractFederationSender):
)
)
+ logger.debug("Successfully handled up to %i", next_token)
await self.store.update_federation_out_pos("events", next_token)
if events:
diff --git a/synapse/federation/transport/client.py b/synapse/federation/transport/client.py
index 1421050b9a..9ce06dfa28 100644
--- a/synapse/federation/transport/client.py
+++ b/synapse/federation/transport/client.py
@@ -229,21 +229,21 @@ class TransportLayerClient:
"""
logger.debug(
"send_data dest=%s, txid=%s",
- transaction.destination, # type: ignore
- transaction.transaction_id, # type: ignore
+ transaction.destination,
+ transaction.transaction_id,
)
- if transaction.destination == self.server_name: # type: ignore
+ if transaction.destination == self.server_name:
raise RuntimeError("Transport layer cannot send to itself!")
# FIXME: This is only used by the tests. The actual json sent is
# generated by the json_data_callback.
json_data = transaction.get_dict()
- path = _create_v1_path("/send/%s", transaction.transaction_id) # type: ignore
+ path = _create_v1_path("/send/%s", transaction.transaction_id)
return await self.client.put_json(
- transaction.destination, # type: ignore
+ transaction.destination,
path=path,
data=json_data,
json_data_callback=json_data_callback,
diff --git a/synapse/handlers/account_validity.py b/synapse/handlers/account_validity.py
index 05a138410e..33e45e3a11 100644
--- a/synapse/handlers/account_validity.py
+++ b/synapse/handlers/account_validity.py
@@ -23,6 +23,7 @@ from synapse.api.errors import AuthError, StoreError, SynapseError
from synapse.metrics.background_process_metrics import wrap_as_background_process
from synapse.types import UserID
from synapse.util import stringutils
+from synapse.util.async_helpers import delay_cancellation
if TYPE_CHECKING:
from synapse.server import HomeServer
@@ -150,7 +151,7 @@ class AccountValidityHandler:
Whether the user has expired.
"""
for callback in self._is_user_expired_callbacks:
- expired = await callback(user_id)
+ expired = await delay_cancellation(callback(user_id))
if expired is not None:
return expired
diff --git a/synapse/handlers/appservice.py b/synapse/handlers/appservice.py
index 1b57840506..85bd5e4768 100644
--- a/synapse/handlers/appservice.py
+++ b/synapse/handlers/appservice.py
@@ -59,7 +59,7 @@ class ApplicationServicesHandler:
self.scheduler = hs.get_application_service_scheduler()
self.started_scheduler = False
self.clock = hs.get_clock()
- self.notify_appservices = hs.config.appservice.notify_appservices
+ self.notify_appservices = hs.config.worker.should_notify_appservices
self.event_sources = hs.get_event_sources()
self._msc2409_to_device_messages_enabled = (
hs.config.experimental.msc2409_to_device_messages_enabled
@@ -416,7 +416,7 @@ class ApplicationServicesHandler:
return typing
async def _handle_receipts(
- self, service: ApplicationService, new_token: Optional[int]
+ self, service: ApplicationService, new_token: int
) -> List[JsonDict]:
"""
Return the latest read receipts that the given application service should receive.
@@ -447,7 +447,7 @@ class ApplicationServicesHandler:
receipts_source = self.event_sources.sources.receipt
receipts, _ = await receipts_source.get_new_events_as(
- service=service, from_key=from_key
+ service=service, from_key=from_key, to_key=new_token
)
return receipts
diff --git a/synapse/handlers/auth.py b/synapse/handlers/auth.py
index 86991d26ce..1b9050ea96 100644
--- a/synapse/handlers/auth.py
+++ b/synapse/handlers/auth.py
@@ -41,6 +41,7 @@ import pymacaroons
import unpaddedbase64
from pymacaroons.exceptions import MacaroonVerificationFailedException
+from twisted.internet.defer import CancelledError
from twisted.web.server import Request
from synapse.api.constants import LoginType
@@ -67,7 +68,7 @@ from synapse.metrics.background_process_metrics import run_as_background_process
from synapse.storage.roommember import ProfileInfo
from synapse.types import JsonDict, Requester, UserID
from synapse.util import stringutils as stringutils
-from synapse.util.async_helpers import maybe_awaitable
+from synapse.util.async_helpers import delay_cancellation, maybe_awaitable
from synapse.util.macaroons import get_value_from_macaroon, satisfy_expiry
from synapse.util.msisdn import phone_number_to_msisdn
from synapse.util.stringutils import base62_encode
@@ -481,7 +482,7 @@ class AuthHandler:
sid = authdict["session"]
# Convert the URI and method to strings.
- uri = request.uri.decode("utf-8") # type: ignore
+ uri = request.uri.decode("utf-8")
method = request.method.decode("utf-8")
# If there's no session ID, create a new session.
@@ -551,7 +552,7 @@ class AuthHandler:
await self.store.set_ui_auth_clientdict(sid, clientdict)
user_agent = get_request_user_agent(request)
- clientip = request.getClientIP()
+ clientip = request.getClientAddress().host
await self.store.add_user_agent_ip_to_ui_auth_session(
session.session_id, user_agent, clientip
@@ -2202,7 +2203,11 @@ class PasswordAuthProvider:
# other than None (i.e. until a callback returns a success)
for callback in self.auth_checker_callbacks[login_type]:
try:
- result = await callback(username, login_type, login_dict)
+ result = await delay_cancellation(
+ callback(username, login_type, login_dict)
+ )
+ except CancelledError:
+ raise
except Exception as e:
logger.warning("Failed to run module API callback %s: %s", callback, e)
continue
@@ -2263,7 +2268,9 @@ class PasswordAuthProvider:
for callback in self.check_3pid_auth_callbacks:
try:
- result = await callback(medium, address, password)
+ result = await delay_cancellation(callback(medium, address, password))
+ except CancelledError:
+ raise
except Exception as e:
logger.warning("Failed to run module API callback %s: %s", callback, e)
continue
@@ -2345,7 +2352,7 @@ class PasswordAuthProvider:
"""
for callback in self.get_username_for_registration_callbacks:
try:
- res = await callback(uia_results, params)
+ res = await delay_cancellation(callback(uia_results, params))
if isinstance(res, str):
return res
@@ -2359,6 +2366,8 @@ class PasswordAuthProvider:
callback,
res,
)
+ except CancelledError:
+ raise
except Exception as e:
logger.error(
"Module raised an exception in get_username_for_registration: %s",
@@ -2388,7 +2397,7 @@ class PasswordAuthProvider:
"""
for callback in self.get_displayname_for_registration_callbacks:
try:
- res = await callback(uia_results, params)
+ res = await delay_cancellation(callback(uia_results, params))
if isinstance(res, str):
return res
@@ -2402,6 +2411,8 @@ class PasswordAuthProvider:
callback,
res,
)
+ except CancelledError:
+ raise
except Exception as e:
logger.error(
"Module raised an exception in get_displayname_for_registration: %s",
@@ -2429,7 +2440,7 @@ class PasswordAuthProvider:
"""
for callback in self.is_3pid_allowed_callbacks:
try:
- res = await callback(medium, address, registration)
+ res = await delay_cancellation(callback(medium, address, registration))
if res is False:
return res
@@ -2443,6 +2454,8 @@ class PasswordAuthProvider:
callback,
res,
)
+ except CancelledError:
+ raise
except Exception as e:
logger.error("Module raised an exception in is_3pid_allowed: %s", e)
raise SynapseError(code=500, msg="Internal Server Error")
diff --git a/synapse/handlers/events.py b/synapse/handlers/events.py
index 5b94b00bc3..82a5aac3dd 100644
--- a/synapse/handlers/events.py
+++ b/synapse/handlers/events.py
@@ -164,7 +164,7 @@ class EventHandler:
event.
"""
redact_behaviour = (
- EventRedactBehaviour.AS_IS if show_redacted else EventRedactBehaviour.REDACT
+ EventRedactBehaviour.as_is if show_redacted else EventRedactBehaviour.redact
)
event = await self.store.get_event(
event_id, check_room_id=room_id, redact_behaviour=redact_behaviour
diff --git a/synapse/handlers/federation.py b/synapse/handlers/federation.py
index d2ba70a814..38dc5b1f6e 100644
--- a/synapse/handlers/federation.py
+++ b/synapse/handlers/federation.py
@@ -316,7 +316,7 @@ class FederationHandler:
events_to_check = await self.store.get_events_as_list(
event_ids_to_check,
- redact_behaviour=EventRedactBehaviour.AS_IS,
+ redact_behaviour=EventRedactBehaviour.as_is,
get_prev_content=False,
)
@@ -1494,7 +1494,7 @@ class FederationHandler:
events = await self.store.get_events_as_list(
batch,
- redact_behaviour=EventRedactBehaviour.AS_IS,
+ redact_behaviour=EventRedactBehaviour.as_is,
allow_rejected=True,
)
for event in events:
diff --git a/synapse/handlers/federation_event.py b/synapse/handlers/federation_event.py
index 693b544286..6cf927e4ff 100644
--- a/synapse/handlers/federation_event.py
+++ b/synapse/handlers/federation_event.py
@@ -860,7 +860,7 @@ class FederationEventHandler:
evs = await self._store.get_events(
list(state_map.values()),
get_prev_content=False,
- redact_behaviour=EventRedactBehaviour.AS_IS,
+ redact_behaviour=EventRedactBehaviour.as_is,
)
event_map.update(evs)
diff --git a/synapse/handlers/identity.py b/synapse/handlers/identity.py
index c183e9c465..9bca2bc4b2 100644
--- a/synapse/handlers/identity.py
+++ b/synapse/handlers/identity.py
@@ -92,7 +92,7 @@ class IdentityHandler:
"""
await self._3pid_validation_ratelimiter_ip.ratelimit(
- None, (medium, request.getClientIP())
+ None, (medium, request.getClientAddress().host)
)
await self._3pid_validation_ratelimiter_address.ratelimit(
None, (medium, address)
diff --git a/synapse/handlers/initial_sync.py b/synapse/handlers/initial_sync.py
index a7db8feb57..7b94770f97 100644
--- a/synapse/handlers/initial_sync.py
+++ b/synapse/handlers/initial_sync.py
@@ -143,7 +143,7 @@ class InitialSyncHandler:
to_key=int(now_token.receipt_key),
)
if self.hs.config.experimental.msc2285_enabled:
- receipt = ReceiptEventSource.filter_out_hidden(receipt, user_id)
+ receipt = ReceiptEventSource.filter_out_private(receipt, user_id)
tags_by_room = await self.store.get_tags_for_user(user_id)
@@ -449,7 +449,7 @@ class InitialSyncHandler:
if not receipts:
return []
if self.hs.config.experimental.msc2285_enabled:
- receipts = ReceiptEventSource.filter_out_hidden(receipts, user_id)
+ receipts = ReceiptEventSource.filter_out_private(receipts, user_id)
return receipts
presence, receipts, (messages, token) = await make_deferred_yieldable(
diff --git a/synapse/handlers/message.py b/synapse/handlers/message.py
index 549b776d6b..e1082954cc 100644
--- a/synapse/handlers/message.py
+++ b/synapse/handlers/message.py
@@ -1407,7 +1407,7 @@ class EventCreationHandler:
original_event = await self.store.get_event(
event.redacts,
- redact_behaviour=EventRedactBehaviour.AS_IS,
+ redact_behaviour=EventRedactBehaviour.as_is,
get_prev_content=False,
allow_rejected=False,
allow_none=True,
@@ -1427,7 +1427,7 @@ class EventCreationHandler:
# Validate a newly added alias or newly added alt_aliases.
original_alias = None
- original_alt_aliases: List[str] = []
+ original_alt_aliases: object = []
original_event_id = event.unsigned.get("replaces_state")
if original_event_id:
@@ -1455,6 +1455,7 @@ class EventCreationHandler:
# If the old version of alt_aliases is of an unknown form,
# completely replace it.
if not isinstance(original_alt_aliases, (list, tuple)):
+ # TODO: check that the original_alt_aliases' entries are all strings
original_alt_aliases = []
# Check that each alias is currently valid.
@@ -1504,7 +1505,7 @@ class EventCreationHandler:
original_event = await self.store.get_event(
event.redacts,
- redact_behaviour=EventRedactBehaviour.AS_IS,
+ redact_behaviour=EventRedactBehaviour.as_is,
get_prev_content=False,
allow_rejected=False,
allow_none=True,
diff --git a/synapse/handlers/oidc.py b/synapse/handlers/oidc.py
index 724b9cfcb4..f6ffb7d18d 100644
--- a/synapse/handlers/oidc.py
+++ b/synapse/handlers/oidc.py
@@ -966,7 +966,7 @@ class OidcProvider:
"Mapping provider does not support de-duplicating Matrix IDs"
)
- attributes = await self._user_mapping_provider.map_user_attributes( # type: ignore
+ attributes = await self._user_mapping_provider.map_user_attributes(
userinfo, token
)
diff --git a/synapse/handlers/presence.py b/synapse/handlers/presence.py
index d078162c29..268481ec19 100644
--- a/synapse/handlers/presence.py
+++ b/synapse/handlers/presence.py
@@ -659,27 +659,28 @@ class PresenceHandler(BasePresenceHandler):
)
now = self.clock.time_msec()
- for state in self.user_to_current_state.values():
- self.wheel_timer.insert(
- now=now, obj=state.user_id, then=state.last_active_ts + IDLE_TIMER
- )
- self.wheel_timer.insert(
- now=now,
- obj=state.user_id,
- then=state.last_user_sync_ts + SYNC_ONLINE_TIMEOUT,
- )
- if self.is_mine_id(state.user_id):
+ if self._presence_enabled:
+ for state in self.user_to_current_state.values():
+ self.wheel_timer.insert(
+ now=now, obj=state.user_id, then=state.last_active_ts + IDLE_TIMER
+ )
self.wheel_timer.insert(
now=now,
obj=state.user_id,
- then=state.last_federation_update_ts + FEDERATION_PING_INTERVAL,
- )
- else:
- self.wheel_timer.insert(
- now=now,
- obj=state.user_id,
- then=state.last_federation_update_ts + FEDERATION_TIMEOUT,
+ then=state.last_user_sync_ts + SYNC_ONLINE_TIMEOUT,
)
+ if self.is_mine_id(state.user_id):
+ self.wheel_timer.insert(
+ now=now,
+ obj=state.user_id,
+ then=state.last_federation_update_ts + FEDERATION_PING_INTERVAL,
+ )
+ else:
+ self.wheel_timer.insert(
+ now=now,
+ obj=state.user_id,
+ then=state.last_federation_update_ts + FEDERATION_TIMEOUT,
+ )
# Set of users who have presence in the `user_to_current_state` that
# have not yet been persisted
@@ -804,6 +805,13 @@ class PresenceHandler(BasePresenceHandler):
This is currently used to bump the max presence stream ID without changing any
user's presence (see PresenceHandler.add_users_to_send_full_presence_to).
"""
+ if not self._presence_enabled:
+ # We shouldn't get here if presence is disabled, but we check anyway
+ # to ensure that we don't a) send out presence federation and b)
+ # don't add things to the wheel timer that will never be handled.
+ logger.warning("Tried to update presence states when presence is disabled")
+ return
+
now = self.clock.time_msec()
with Measure(self.clock, "presence_update_states"):
@@ -1229,6 +1237,10 @@ class PresenceHandler(BasePresenceHandler):
):
raise SynapseError(400, "Invalid presence state")
+ # If presence is disabled, no-op
+ if not self.hs.config.server.use_presence:
+ return
+
user_id = target_user.to_string()
prev_state = await self.current_state_for_user(user_id)
diff --git a/synapse/handlers/push_rules.py b/synapse/handlers/push_rules.py
new file mode 100644
index 0000000000..2599160bcc
--- /dev/null
+++ b/synapse/handlers/push_rules.py
@@ -0,0 +1,138 @@
+# Copyright 2022 The Matrix.org Foundation C.I.C.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+from typing import TYPE_CHECKING, List, Optional, Union
+
+import attr
+
+from synapse.api.errors import SynapseError, UnrecognizedRequestError
+from synapse.push.baserules import BASE_RULE_IDS
+from synapse.storage.push_rule import RuleNotFoundException
+from synapse.types import JsonDict
+
+if TYPE_CHECKING:
+ from synapse.server import HomeServer
+
+
+@attr.s(slots=True, frozen=True, auto_attribs=True)
+class RuleSpec:
+ scope: str
+ template: str
+ rule_id: str
+ attr: Optional[str]
+
+
+class PushRulesHandler:
+ """A class to handle changes in push rules for users."""
+
+ def __init__(self, hs: "HomeServer"):
+ self._notifier = hs.get_notifier()
+ self._main_store = hs.get_datastores().main
+
+ async def set_rule_attr(
+ self, user_id: str, spec: RuleSpec, val: Union[bool, JsonDict]
+ ) -> None:
+ """Set an attribute (enabled or actions) on an existing push rule.
+
+ Notifies listeners (e.g. sync handler) of the change.
+
+ Args:
+ user_id: the user for which to modify the push rule.
+ spec: the spec of the push rule to modify.
+ val: the value to change the attribute to.
+
+ Raises:
+ RuleNotFoundException if the rule being modified doesn't exist.
+ SynapseError(400) if the value is malformed.
+ UnrecognizedRequestError if the attribute to change is unknown.
+ InvalidRuleException if we're trying to change the actions on a rule but
+ the provided actions aren't compliant with the spec.
+ """
+ if spec.attr not in ("enabled", "actions"):
+ # for the sake of potential future expansion, shouldn't report
+ # 404 in the case of an unknown request so check it corresponds to
+ # a known attribute first.
+ raise UnrecognizedRequestError()
+
+ namespaced_rule_id = f"global/{spec.template}/{spec.rule_id}"
+ rule_id = spec.rule_id
+ is_default_rule = rule_id.startswith(".")
+ if is_default_rule:
+ if namespaced_rule_id not in BASE_RULE_IDS:
+ raise RuleNotFoundException("Unknown rule %r" % (namespaced_rule_id,))
+ if spec.attr == "enabled":
+ if isinstance(val, dict) and "enabled" in val:
+ val = val["enabled"]
+ if not isinstance(val, bool):
+ # Legacy fallback
+ # This should *actually* take a dict, but many clients pass
+ # bools directly, so let's not break them.
+ raise SynapseError(400, "Value for 'enabled' must be boolean")
+ await self._main_store.set_push_rule_enabled(
+ user_id, namespaced_rule_id, val, is_default_rule
+ )
+ elif spec.attr == "actions":
+ if not isinstance(val, dict):
+ raise SynapseError(400, "Value must be a dict")
+ actions = val.get("actions")
+ if not isinstance(actions, list):
+ raise SynapseError(400, "Value for 'actions' must be dict")
+ check_actions(actions)
+ rule_id = spec.rule_id
+ is_default_rule = rule_id.startswith(".")
+ if is_default_rule:
+ if namespaced_rule_id not in BASE_RULE_IDS:
+ raise RuleNotFoundException(
+ "Unknown rule %r" % (namespaced_rule_id,)
+ )
+ await self._main_store.set_push_rule_actions(
+ user_id, namespaced_rule_id, actions, is_default_rule
+ )
+ else:
+ raise UnrecognizedRequestError()
+
+ self.notify_user(user_id)
+
+ def notify_user(self, user_id: str) -> None:
+ """Notify listeners about a push rule change.
+
+ Args:
+ user_id: the user ID the change is for.
+ """
+ stream_id = self._main_store.get_max_push_rules_stream_id()
+ self._notifier.on_new_event("push_rules_key", stream_id, users=[user_id])
+
+
+def check_actions(actions: List[Union[str, JsonDict]]) -> None:
+ """Check if the given actions are spec compliant.
+
+ Args:
+ actions: the actions to check.
+
+ Raises:
+ InvalidRuleException if the rules aren't compliant with the spec.
+ """
+ if not isinstance(actions, list):
+ raise InvalidRuleException("No actions found")
+
+ for a in actions:
+ if a in ["notify", "dont_notify", "coalesce"]:
+ pass
+ elif isinstance(a, dict) and "set_tweak" in a:
+ pass
+ else:
+ raise InvalidRuleException("Unrecognised action %s" % a)
+
+
+class InvalidRuleException(Exception):
+ pass
diff --git a/synapse/handlers/receipts.py b/synapse/handlers/receipts.py
index 6250bb3bdf..43d615357b 100644
--- a/synapse/handlers/receipts.py
+++ b/synapse/handlers/receipts.py
@@ -14,7 +14,7 @@
import logging
from typing import TYPE_CHECKING, Iterable, List, Optional, Tuple
-from synapse.api.constants import ReadReceiptEventFields, ReceiptTypes
+from synapse.api.constants import ReceiptTypes
from synapse.appservice import ApplicationService
from synapse.streams import EventSource
from synapse.types import JsonDict, ReadReceipt, UserID, get_domain_from_id
@@ -112,7 +112,7 @@ class ReceiptsHandler:
)
if not res:
- # res will be None if this read receipt is 'old'
+ # res will be None if this receipt is 'old'
continue
stream_id, max_persisted_id = res
@@ -138,7 +138,7 @@ class ReceiptsHandler:
return True
async def received_client_receipt(
- self, room_id: str, receipt_type: str, user_id: str, event_id: str, hidden: bool
+ self, room_id: str, receipt_type: str, user_id: str, event_id: str
) -> None:
"""Called when a client tells us a local user has read up to the given
event_id in the room.
@@ -148,16 +148,14 @@ class ReceiptsHandler:
receipt_type=receipt_type,
user_id=user_id,
event_ids=[event_id],
- data={"ts": int(self.clock.time_msec()), "hidden": hidden},
+ data={"ts": int(self.clock.time_msec())},
)
is_new = await self._handle_new_receipts([receipt])
if not is_new:
return
- if self.federation_sender and not (
- self.hs.config.experimental.msc2285_enabled and hidden
- ):
+ if self.federation_sender and receipt_type != ReceiptTypes.READ_PRIVATE:
await self.federation_sender.send_read_receipt(receipt)
@@ -167,46 +165,37 @@ class ReceiptEventSource(EventSource[int, JsonDict]):
self.config = hs.config
@staticmethod
- def filter_out_hidden(events: List[JsonDict], user_id: str) -> List[JsonDict]:
+ def filter_out_private(events: List[JsonDict], user_id: str) -> List[JsonDict]:
+ """
+ This method takes in what is returned by
+ get_linearized_receipts_for_rooms() and goes through read receipts
+ filtering out m.read.private receipts if they were not sent by the
+ current user.
+ """
+
visible_events = []
- # filter out hidden receipts the user shouldn't see
+ # filter out private receipts the user shouldn't see
for event in events:
content = event.get("content", {})
new_event = event.copy()
new_event["content"] = {}
- for event_id in content.keys():
- event_content = content.get(event_id, {})
- m_read = event_content.get(ReceiptTypes.READ, {})
+ for event_id, event_content in content.items():
+ receipt_event = {}
+ for receipt_type, receipt_content in event_content.items():
+ if receipt_type == ReceiptTypes.READ_PRIVATE:
+ user_rr = receipt_content.get(user_id, None)
+ if user_rr:
+ receipt_event[ReceiptTypes.READ_PRIVATE] = {
+ user_id: user_rr.copy()
+ }
+ else:
+ receipt_event[receipt_type] = receipt_content.copy()
- # If m_read is missing copy over the original event_content as there is nothing to process here
- if not m_read:
- new_event["content"][event_id] = event_content.copy()
- continue
-
- new_users = {}
- for rr_user_id, user_rr in m_read.items():
- try:
- hidden = user_rr.get("hidden")
- except AttributeError:
- # Due to https://github.com/matrix-org/synapse/issues/10376
- # there are cases where user_rr is a string, in those cases
- # we just ignore the read receipt
- continue
-
- if hidden is not True or rr_user_id == user_id:
- new_users[rr_user_id] = user_rr.copy()
- # If hidden has a value replace hidden with the correct prefixed key
- if hidden is not None:
- new_users[rr_user_id].pop("hidden")
- new_users[rr_user_id][
- ReadReceiptEventFields.MSC2285_HIDDEN
- ] = hidden
-
- # Set new users unless empty
- if len(new_users.keys()) > 0:
- new_event["content"][event_id] = {ReceiptTypes.READ: new_users}
+ # Only include the receipt event if it is non-empty.
+ if receipt_event:
+ new_event["content"][event_id] = receipt_event
# Append new_event to visible_events unless empty
if len(new_event["content"].keys()) > 0:
@@ -234,18 +223,19 @@ class ReceiptEventSource(EventSource[int, JsonDict]):
)
if self.config.experimental.msc2285_enabled:
- events = ReceiptEventSource.filter_out_hidden(events, user.to_string())
+ events = ReceiptEventSource.filter_out_private(events, user.to_string())
return events, to_key
async def get_new_events_as(
- self, from_key: int, service: ApplicationService
+ self, from_key: int, to_key: int, service: ApplicationService
) -> Tuple[List[JsonDict], int]:
"""Returns a set of new read receipt events that an appservice
may be interested in.
Args:
from_key: the stream position at which events should be fetched from
+ to_key: the stream position up to which events should be fetched to
service: The appservice which may be interested
Returns:
@@ -255,7 +245,6 @@ class ReceiptEventSource(EventSource[int, JsonDict]):
* The current read receipt stream token.
"""
from_key = int(from_key)
- to_key = self.get_current_key()
if from_key == to_key:
return [], to_key
diff --git a/synapse/handlers/relations.py b/synapse/handlers/relations.py
index 5efb561273..c2754ec918 100644
--- a/synapse/handlers/relations.py
+++ b/synapse/handlers/relations.py
@@ -11,6 +11,7 @@
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
+import collections.abc
import logging
from typing import (
TYPE_CHECKING,
@@ -24,7 +25,6 @@ from typing import (
)
import attr
-from frozendict import frozendict
from synapse.api.constants import RelationTypes
from synapse.api.errors import SynapseError
@@ -44,8 +44,6 @@ logger = logging.getLogger(__name__)
class _ThreadAggregation:
# The latest event in the thread.
latest_event: EventBase
- # The latest edit to the latest event in the thread.
- latest_edit: Optional[EventBase]
# The total number of events in the thread.
count: int
# True if the current user has sent an event to the thread.
@@ -295,7 +293,7 @@ class RelationsHandler:
for event_id, summary in summaries.items():
if summary:
- thread_count, latest_thread_event, edit = summary
+ thread_count, latest_thread_event = summary
# Subtract off the count of any ignored users.
for ignored_user in ignored_users:
@@ -340,7 +338,6 @@ class RelationsHandler:
results[event_id] = _ThreadAggregation(
latest_event=latest_thread_event,
- latest_edit=edit,
count=thread_count,
# If there's a thread summary it must also exist in the
# participated dictionary.
@@ -359,15 +356,37 @@ class RelationsHandler:
user_id: The user requesting the bundled aggregations.
Returns:
- A map of event ID to the bundled aggregation for the event. Not all
- events may have bundled aggregations in the results.
+ A map of event ID to the bundled aggregations for the event.
+
+ Not all requested events may exist in the results (if they don't have
+ bundled aggregations).
+
+ The results may include additional events which are related to the
+ requested events.
"""
- # De-duplicate events by ID to handle the same event requested multiple times.
- #
- # State events do not get bundled aggregations.
- events_by_id = {
- event.event_id: event for event in events if not event.is_state()
- }
+ # De-duplicated events by ID to handle the same event requested multiple times.
+ events_by_id = {}
+ # A map of event ID to the relation in that event, if there is one.
+ relations_by_id: Dict[str, str] = {}
+ for event in events:
+ # State events do not get bundled aggregations.
+ if event.is_state():
+ continue
+
+ relates_to = event.content.get("m.relates_to")
+ relation_type = None
+ if isinstance(relates_to, collections.abc.Mapping):
+ relation_type = relates_to.get("rel_type")
+ # An event which is a replacement (ie edit) or annotation (ie,
+ # reaction) may not have any other event related to it.
+ if relation_type in (RelationTypes.ANNOTATION, RelationTypes.REPLACE):
+ continue
+
+ # The event should get bundled aggregations.
+ events_by_id[event.event_id] = event
+ # Track the event's relation information for later.
+ if isinstance(relation_type, str):
+ relations_by_id[event.event_id] = relation_type
# event ID -> bundled aggregation in non-serialized form.
results: Dict[str, BundledAggregations] = {}
@@ -375,16 +394,34 @@ class RelationsHandler:
# Fetch any ignored users of the requesting user.
ignored_users = await self._main_store.ignored_users(user_id)
+ # Threads are special as the latest event of a thread might cause additional
+ # events to be fetched. Thus, we check those first!
+
+ # Fetch thread summaries (but only for the directly requested events).
+ threads = await self.get_threads_for_events(
+ # It is not valid to start a thread on an event which itself relates to another event.
+ [eid for eid in events_by_id.keys() if eid not in relations_by_id],
+ user_id,
+ ignored_users,
+ )
+ for event_id, thread in threads.items():
+ results.setdefault(event_id, BundledAggregations()).thread = thread
+
+ # If the latest event in a thread is not already being fetched,
+ # add it. This ensures that the bundled aggregations for the
+ # latest thread event is correct.
+ latest_thread_event = thread.latest_event
+ if latest_thread_event and latest_thread_event.event_id not in events_by_id:
+ events_by_id[latest_thread_event.event_id] = latest_thread_event
+ # Keep relations_by_id in sync with events_by_id:
+ #
+ # We know that the latest event in a thread has a thread relation
+ # (as that is what makes it part of the thread).
+ relations_by_id[latest_thread_event.event_id] = RelationTypes.THREAD
+
# Fetch other relations per event.
for event in events_by_id.values():
- # Do not bundle aggregations for an event which represents an edit or an
- # annotation. It does not make sense for them to have related events.
- relates_to = event.content.get("m.relates_to")
- if isinstance(relates_to, (dict, frozendict)):
- relation_type = relates_to.get("rel_type")
- if relation_type in (RelationTypes.ANNOTATION, RelationTypes.REPLACE):
- continue
-
+ # Fetch any annotations (ie, reactions) to bundle with this event.
annotations = await self.get_annotations_for_event(
event.event_id, event.room_id, ignored_users=ignored_users
)
@@ -393,6 +430,7 @@ class RelationsHandler:
event.event_id, BundledAggregations()
).annotations = {"chunk": annotations}
+ # Fetch any references to bundle with this event.
references, next_token = await self.get_relations_for_event(
event.event_id,
event,
@@ -425,10 +463,4 @@ class RelationsHandler:
for event_id, edit in edits.items():
results.setdefault(event_id, BundledAggregations()).replace = edit
- threads = await self.get_threads_for_events(
- events_by_id.keys(), user_id, ignored_users
- )
- for event_id, thread in threads.items():
- results.setdefault(event_id, BundledAggregations()).thread = thread
-
return results
diff --git a/synapse/handlers/room.py b/synapse/handlers/room.py
index b31f00b517..604eb6ec15 100644
--- a/synapse/handlers/room.py
+++ b/synapse/handlers/room.py
@@ -57,7 +57,7 @@ from synapse.api.filtering import Filter
from synapse.api.room_versions import KNOWN_ROOM_VERSIONS, RoomVersion
from synapse.event_auth import validate_event_for_room_version
from synapse.events import EventBase
-from synapse.events.utils import copy_power_levels_contents
+from synapse.events.utils import copy_and_fixup_power_levels_contents
from synapse.federation.federation_client import InvalidResponseError
from synapse.handlers.federation import get_domains_from_state
from synapse.handlers.relations import BundledAggregations
@@ -337,13 +337,13 @@ class RoomCreationHandler:
# 50, but if the default PL in a room is 50 or more, then we set the
# required PL above that.
- pl_content = dict(old_room_pl_state.content)
- users_default = int(pl_content.get("users_default", 0))
+ pl_content = copy_and_fixup_power_levels_contents(old_room_pl_state.content)
+ users_default: int = pl_content.get("users_default", 0) # type: ignore[assignment]
restricted_level = max(users_default + 1, 50)
updated = False
for v in ("invite", "events_default"):
- current = int(pl_content.get(v, 0))
+ current: int = pl_content.get(v, 0) # type: ignore[assignment]
if current < restricted_level:
logger.debug(
"Setting level for %s in %s to %i (was %i)",
@@ -380,7 +380,9 @@ class RoomCreationHandler:
"state_key": "",
"room_id": new_room_id,
"sender": requester.user.to_string(),
- "content": old_room_pl_state.content,
+ "content": copy_and_fixup_power_levels_contents(
+ old_room_pl_state.content
+ ),
},
ratelimit=False,
)
@@ -471,7 +473,7 @@ class RoomCreationHandler:
# dict so we can't just copy.deepcopy it.
initial_state[
(EventTypes.PowerLevels, "")
- ] = power_levels = copy_power_levels_contents(
+ ] = power_levels = copy_and_fixup_power_levels_contents(
initial_state[(EventTypes.PowerLevels, "")]
)
diff --git a/synapse/handlers/room_summary.py b/synapse/handlers/room_summary.py
index 486145f48a..ff24ec8063 100644
--- a/synapse/handlers/room_summary.py
+++ b/synapse/handlers/room_summary.py
@@ -105,6 +105,7 @@ class RoomSummaryHandler:
hs.get_clock(),
"get_room_hierarchy",
)
+ self._msc3266_enabled = hs.config.experimental.msc3266_enabled
async def get_room_hierarchy(
self,
@@ -630,7 +631,7 @@ class RoomSummaryHandler:
return False
async def _is_remote_room_accessible(
- self, requester: str, room_id: str, room: JsonDict
+ self, requester: Optional[str], room_id: str, room: JsonDict
) -> bool:
"""
Calculate whether the room received over federation should be shown to the requester.
@@ -645,7 +646,8 @@ class RoomSummaryHandler:
due to an invite, etc.
Args:
- requester: The user requesting the summary.
+ requester: The user requesting the summary. If not passed only world
+ readability is checked.
room_id: The room ID returned over federation.
room: The summary of the room returned over federation.
@@ -659,6 +661,8 @@ class RoomSummaryHandler:
or room.get("world_readable") is True
):
return True
+ elif not requester:
+ return False
# Check if the user is a member of any of the allowed rooms from the response.
allowed_rooms = room.get("allowed_room_ids")
@@ -715,6 +719,10 @@ class RoomSummaryHandler:
"room_type": create_event.content.get(EventContentFields.ROOM_TYPE),
}
+ if self._msc3266_enabled:
+ entry["im.nheko.summary.version"] = stats["version"]
+ entry["im.nheko.summary.encryption"] = stats["encryption"]
+
# Federation requests need to provide additional information so the
# requested server is able to filter the response appropriately.
if for_federation:
@@ -812,9 +820,45 @@ class RoomSummaryHandler:
room_summary["membership"] = membership or "leave"
else:
- # TODO federation API, descoped from initial unstable implementation
- # as MSC needs more maturing on that side.
- raise SynapseError(400, "Federation is not currently supported.")
+ # Reuse the hierarchy query over federation
+ if remote_room_hosts is None:
+ raise SynapseError(400, "Missing via to query remote room")
+
+ (
+ room_entry,
+ children_room_entries,
+ inaccessible_children,
+ ) = await self._summarize_remote_room_hierarchy(
+ _RoomQueueEntry(room_id, remote_room_hosts),
+ suggested_only=True,
+ )
+
+ # The results over federation might include rooms that we, as the
+ # requesting server, are allowed to see, but the requesting user is
+ # not permitted to see.
+ #
+ # Filter the returned results to only what is accessible to the user.
+ if not room_entry or not await self._is_remote_room_accessible(
+ requester, room_entry.room_id, room_entry.room
+ ):
+ 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
+ # room is purged even for remote rooms.
+ if requester:
+ (
+ membership,
+ _,
+ ) = await self._store.get_local_current_membership_for_user_in_room(
+ requester, room_id
+ )
+ room["membership"] = membership or "leave"
+
+ return room
return room_summary
diff --git a/synapse/handlers/search.py b/synapse/handlers/search.py
index 102dd4b57d..5619f8f50e 100644
--- a/synapse/handlers/search.py
+++ b/synapse/handlers/search.py
@@ -357,7 +357,7 @@ class SearchHandler:
itertools.chain(
# The events_before and events_after for each context.
itertools.chain.from_iterable(
- itertools.chain(context["events_before"], context["events_after"]) # type: ignore[arg-type]
+ itertools.chain(context["events_before"], context["events_after"])
for context in contexts.values()
),
# The returned events.
@@ -373,10 +373,10 @@ class SearchHandler:
for context in contexts.values():
context["events_before"] = self._event_serializer.serialize_events(
- context["events_before"], time_now, bundle_aggregations=aggregations # type: ignore[arg-type]
+ context["events_before"], time_now, bundle_aggregations=aggregations
)
context["events_after"] = self._event_serializer.serialize_events(
- context["events_after"], time_now, bundle_aggregations=aggregations # type: ignore[arg-type]
+ context["events_after"], time_now, bundle_aggregations=aggregations
)
results = [
diff --git a/synapse/handlers/sso.py b/synapse/handlers/sso.py
index e4fe94e557..1e171f3f71 100644
--- a/synapse/handlers/sso.py
+++ b/synapse/handlers/sso.py
@@ -468,7 +468,7 @@ class SsoHandler:
auth_provider_id,
remote_user_id,
get_request_user_agent(request),
- request.getClientIP(),
+ request.getClientAddress().host,
)
new_user = True
elif self._sso_update_profile_information:
@@ -928,7 +928,7 @@ class SsoHandler:
session.auth_provider_id,
session.remote_user_id,
get_request_user_agent(request),
- request.getClientIP(),
+ request.getClientAddress().host,
)
logger.info(
diff --git a/synapse/handlers/sync.py b/synapse/handlers/sync.py
index 5125126a80..2c555a66d0 100644
--- a/synapse/handlers/sync.py
+++ b/synapse/handlers/sync.py
@@ -1045,7 +1045,7 @@ class SyncHandler:
last_unread_event_id = await self.store.get_last_receipt_event_id_for_user(
user_id=sync_config.user.to_string(),
room_id=room_id,
- receipt_type=ReceiptTypes.READ,
+ receipt_types=(ReceiptTypes.READ, ReceiptTypes.READ_PRIVATE),
)
return await self.store.get_unread_event_push_actions_by_room_for_user(
diff --git a/synapse/handlers/ui_auth/checkers.py b/synapse/handlers/ui_auth/checkers.py
index 472b029af3..05cebb5d4d 100644
--- a/synapse/handlers/ui_auth/checkers.py
+++ b/synapse/handlers/ui_auth/checkers.py
@@ -256,7 +256,9 @@ class RegistrationTokenAuthChecker(UserInteractiveAuthChecker):
def __init__(self, hs: "HomeServer"):
super().__init__(hs)
self.hs = hs
- self._enabled = bool(hs.config.registration.registration_requires_token)
+ self._enabled = bool(
+ hs.config.registration.registration_requires_token
+ ) or bool(hs.config.registration.enable_registration_token_3pid_bypass)
self.store = hs.get_datastores().main
def is_enabled(self) -> bool:
diff --git a/synapse/handlers/user_directory.py b/synapse/handlers/user_directory.py
index 048fd4bb82..74f7fdfe6c 100644
--- a/synapse/handlers/user_directory.py
+++ b/synapse/handlers/user_directory.py
@@ -60,7 +60,7 @@ class UserDirectoryHandler(StateDeltasHandler):
self.clock = hs.get_clock()
self.notifier = hs.get_notifier()
self.is_mine_id = hs.is_mine_id
- self.update_user_directory = hs.config.server.update_user_directory
+ self.update_user_directory = hs.config.worker.should_update_user_directory
self.search_all_users = hs.config.userdirectory.user_directory_search_all_users
self.spam_checker = hs.get_spam_checker()
# The current position in the current_state_delta stream
diff --git a/synapse/http/matrixfederationclient.py b/synapse/http/matrixfederationclient.py
index e686445955..c2ec3caa0e 100644
--- a/synapse/http/matrixfederationclient.py
+++ b/synapse/http/matrixfederationclient.py
@@ -73,7 +73,7 @@ from synapse.logging.context import make_deferred_yieldable, run_in_background
from synapse.logging.opentracing import set_tag, start_active_span, tags
from synapse.types import JsonDict
from synapse.util import json_decoder
-from synapse.util.async_helpers import timeout_deferred
+from synapse.util.async_helpers import AwakenableSleeper, timeout_deferred
from synapse.util.metrics import Measure
if TYPE_CHECKING:
@@ -353,6 +353,13 @@ class MatrixFederationHttpClient:
self._cooperator = Cooperator(scheduler=schedule)
+ self._sleeper = AwakenableSleeper(self.reactor)
+
+ def wake_destination(self, destination: str) -> None:
+ """Called when the remote server may have come back online."""
+
+ self._sleeper.wake(destination)
+
async def _send_request_with_optional_trailing_slash(
self,
request: MatrixFederationRequest,
@@ -474,6 +481,8 @@ class MatrixFederationHttpClient:
self._store,
backoff_on_404=backoff_on_404,
ignore_backoff=ignore_backoff,
+ notifier=self.hs.get_notifier(),
+ replication_client=self.hs.get_replication_command_handler(),
)
method_bytes = request.method.encode("ascii")
@@ -664,7 +673,9 @@ class MatrixFederationHttpClient:
delay,
)
- await self.clock.sleep(delay)
+ # Sleep for the calculated delay, or wake up immediately
+ # if we get notified that the server is back up.
+ await self._sleeper.sleep(request.destination, delay * 1000)
retries_left -= 1
else:
raise
diff --git a/synapse/http/server.py b/synapse/http/server.py
index 31ca841889..657bffcddd 100644
--- a/synapse/http/server.py
+++ b/synapse/http/server.py
@@ -43,6 +43,7 @@ from typing_extensions import Protocol
from zope.interface import implementer
from twisted.internet import defer, interfaces
+from twisted.internet.defer import CancelledError
from twisted.python import failure
from twisted.web import resource
from twisted.web.server import NOT_DONE_YET, Request
@@ -82,6 +83,14 @@ HTML_ERROR_TEMPLATE = """