Compare commits
81 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| eab9d3459f | |||
| 0f4128d448 | |||
| fa16df37fc | |||
| c2e1ee17b8 | |||
| 5bfad0f87f | |||
| 19e5d44886 | |||
| 46bd7f4ed9 | |||
| f383b9b3ec | |||
| 434fd82d5f | |||
| 2c5e2ae898 | |||
| e825f7366b | |||
| 953df2ad88 | |||
| 4390121684 | |||
| 05bc059e0d | |||
| 507c1cb330 | |||
| 7a25b4302c | |||
| 12abd72497 | |||
| 2281427175 | |||
| 51c01d450a | |||
| 1b09b0832e | |||
| 1595052b26 | |||
| 54fb517c28 | |||
| 3d1b860f90 | |||
| 5ce2887653 | |||
| c962f87d6f | |||
| 827f0669bf | |||
| 70d3e70009 | |||
| 0f954466c4 | |||
| c97042f7ee | |||
| 7a19995120 | |||
| ab18441573 | |||
| e2ed1b7155 | |||
| 3d2cabf966 | |||
| 026ac4486c | |||
| b6a6bb4027 | |||
| 860fdd9098 | |||
| ec24813220 | |||
| 96d92156d0 | |||
| e9e6aacfbe | |||
| 41320a0554 | |||
| 6dd7fa12dc | |||
| afbdbe0634 | |||
| 166fafdf8d | |||
| a91078200d | |||
| 845732be45 | |||
| a648a06d52 | |||
| 92d21faf12 | |||
| 78a3111c41 | |||
| 503a95804e | |||
| 668597214f | |||
| fb7a2cc4cc | |||
| d6e94ad9d9 | |||
| 570bf32bbb | |||
| 5eccfdfafd | |||
| ec6758d472 | |||
| 1c910e2216 | |||
| 8d317f6da5 | |||
| a2a867b521 | |||
| c2f4871226 | |||
| cb209638ea | |||
| 4e80ca2243 | |||
| e17e5c97e0 | |||
| f8e7a9418a | |||
| 224d792dd7 | |||
| 05aeeb3a80 | |||
| b817574be7 | |||
| 23768ccb4d | |||
| d548d8f18d | |||
| 24ef1460f6 | |||
| 583f22780f | |||
| 922b771337 | |||
| 502f075e96 | |||
| 39be5bc550 | |||
| 4f3082d6bf | |||
| bf3115584c | |||
| 543dc9c93e | |||
| 6236afc621 | |||
| 57d334a13d | |||
| ca3db044a3 | |||
| 335ebb21cc | |||
| 8b603299bf |
@@ -135,11 +135,42 @@ jobs:
|
||||
/logs/**/*.log*
|
||||
|
||||
|
||||
# TODO: run complement (as with twisted trunk, see #12473).
|
||||
complement:
|
||||
if: "${{ !failure() && !cancelled() }}"
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
# open an issue if the build fails, so we know about it.
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- arrangement: monolith
|
||||
database: SQLite
|
||||
|
||||
- arrangement: monolith
|
||||
database: Postgres
|
||||
|
||||
- arrangement: workers
|
||||
database: Postgres
|
||||
|
||||
steps:
|
||||
- name: Run actions/checkout@v2 for synapse
|
||||
uses: actions/checkout@v2
|
||||
with:
|
||||
path: synapse
|
||||
|
||||
- name: Prepare Complement's Prerequisites
|
||||
run: synapse/.ci/scripts/setup_complement_prerequisites.sh
|
||||
|
||||
- run: |
|
||||
set -o pipefail
|
||||
TEST_ONLY_IGNORE_POETRY_LOCKFILE=1 POSTGRES=${{ (matrix.database == 'Postgres') && 1 || '' }} WORKERS=${{ (matrix.arrangement == 'workers') && 1 || '' }} COMPLEMENT_DIR=`pwd`/complement synapse/scripts-dev/complement.sh -json 2>&1 | gotestfmt
|
||||
shell: bash
|
||||
name: Run Complement Tests
|
||||
|
||||
# Open an issue if the build fails, so we know about it.
|
||||
# Only do this if we're not experimenting with this action in a PR.
|
||||
open-issue:
|
||||
if: failure()
|
||||
if: "failure() && github.event_name != 'push' && github.event_name != 'pull_request'"
|
||||
needs:
|
||||
# TODO: should mypy be included here? It feels more brittle than the other two.
|
||||
- mypy
|
||||
|
||||
@@ -328,6 +328,9 @@ jobs:
|
||||
- arrangement: monolith
|
||||
database: Postgres
|
||||
|
||||
- arrangement: workers
|
||||
database: Postgres
|
||||
|
||||
steps:
|
||||
- name: Run actions/checkout@v2 for synapse
|
||||
uses: actions/checkout@v2
|
||||
@@ -343,30 +346,6 @@ jobs:
|
||||
shell: bash
|
||||
name: Run Complement Tests
|
||||
|
||||
# XXX When complement with workers is stable, move this back into the standard
|
||||
# "complement" matrix above.
|
||||
#
|
||||
# See https://github.com/matrix-org/synapse/issues/13161
|
||||
complement-workers:
|
||||
if: "${{ !failure() && !cancelled() }}"
|
||||
needs: linting-done
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Run actions/checkout@v2 for synapse
|
||||
uses: actions/checkout@v2
|
||||
with:
|
||||
path: synapse
|
||||
|
||||
- name: Prepare Complement's Prerequisites
|
||||
run: synapse/.ci/scripts/setup_complement_prerequisites.sh
|
||||
|
||||
- run: |
|
||||
set -o pipefail
|
||||
POSTGRES=1 WORKERS=1 COMPLEMENT_DIR=`pwd`/complement synapse/scripts-dev/complement.sh -json 2>&1 | gotestfmt
|
||||
shell: bash
|
||||
name: Run Complement Tests
|
||||
|
||||
# a job which marks all the other jobs as complete, thus allowing PRs to be merged.
|
||||
tests-done:
|
||||
if: ${{ always() }}
|
||||
|
||||
+99
-1
@@ -1,7 +1,105 @@
|
||||
Synapse 1.65.0rc2 (2022-08-11)
|
||||
==============================
|
||||
|
||||
Internal Changes
|
||||
----------------
|
||||
|
||||
- Revert 'Remove the unspecced `room_id` field in the `/hierarchy` response. ([\#13365](https://github.com/matrix-org/synapse/issues/13365))' to give more time for clients to update. ([\#13501](https://github.com/matrix-org/synapse/issues/13501))
|
||||
|
||||
|
||||
Synapse 1.65.0rc1 (2022-08-09)
|
||||
==============================
|
||||
|
||||
Features
|
||||
--------
|
||||
|
||||
- Add support for stable prefixes for [MSC2285 (private read receipts)](https://github.com/matrix-org/matrix-spec-proposals/pull/2285). ([\#13273](https://github.com/matrix-org/synapse/issues/13273))
|
||||
- Add new unstable error codes `ORG.MATRIX.MSC3848.ALREADY_JOINED`, `ORG.MATRIX.MSC3848.NOT_JOINED`, and `ORG.MATRIX.MSC3848.INSUFFICIENT_POWER` described in [MSC3848](https://github.com/matrix-org/matrix-spec-proposals/pull/3848). ([\#13343](https://github.com/matrix-org/synapse/issues/13343))
|
||||
- Use stable prefixes for [MSC3827](https://github.com/matrix-org/matrix-spec-proposals/pull/3827). ([\#13370](https://github.com/matrix-org/synapse/issues/13370))
|
||||
- Add a new module API method to translate a room alias into a room ID. ([\#13428](https://github.com/matrix-org/synapse/issues/13428))
|
||||
- Add a new module API method to create a room. ([\#13429](https://github.com/matrix-org/synapse/issues/13429))
|
||||
- Add remote join capability to the module API's `update_room_membership` method (in a backwards compatible manner). ([\#13441](https://github.com/matrix-org/synapse/issues/13441))
|
||||
|
||||
|
||||
Bugfixes
|
||||
--------
|
||||
|
||||
- Update the version of the LDAP3 auth provider module included in the `matrixdotorg/synapse` DockerHub images and the Debian packages hosted on packages.matrix.org to 0.2.2. This version fixes a regression in the module. ([\#13470](https://github.com/matrix-org/synapse/issues/13470))
|
||||
- Fix a bug introduced in Synapse v1.41.0 where the `/hierarchy` API returned non-standard information (a `room_id` field under each entry in `children_state`). ([\#13365](https://github.com/matrix-org/synapse/issues/13365))
|
||||
- Fix a bug introduced in Synapse 0.24.0 that would respond with the wrong error status code to `/joined_members` requests when the requester is not a current member of the room. Contributed by @andrewdoh. ([\#13374](https://github.com/matrix-org/synapse/issues/13374))
|
||||
- Fix bug in handling of typing events for appservices. Contributed by Nick @ Beeper (@fizzadar). ([\#13392](https://github.com/matrix-org/synapse/issues/13392))
|
||||
- Fix a bug introduced in Synapse 1.57.0 where rooms listed in `exclude_rooms_from_sync` in the configuration file would not be properly excluded from incremental syncs. ([\#13408](https://github.com/matrix-org/synapse/issues/13408))
|
||||
- Fix a bug in the experimental faster-room-joins support which could cause it to get stuck in an infinite loop. ([\#13353](https://github.com/matrix-org/synapse/issues/13353))
|
||||
- Faster room joins: fix a bug which caused rejected events to become un-rejected during state syncing. ([\#13413](https://github.com/matrix-org/synapse/issues/13413))
|
||||
- Faster room joins: fix error when running out of servers to sync partial state with, so that Synapse raises the intended error instead. ([\#13432](https://github.com/matrix-org/synapse/issues/13432))
|
||||
|
||||
|
||||
Updates to the Docker image
|
||||
---------------------------
|
||||
|
||||
- Make Docker images build on armv7 by installing cryptography dependencies in the 'requirements' stage. Contributed by Jasper Spaans. ([\#13372](https://github.com/matrix-org/synapse/issues/13372))
|
||||
|
||||
|
||||
Improved Documentation
|
||||
----------------------
|
||||
|
||||
- Update the 'registration tokens' page to acknowledge that the relevant MSC was merged into version 1.2 of the Matrix specification. Contributed by @moan0s. ([\#11897](https://github.com/matrix-org/synapse/issues/11897))
|
||||
- Document which HTTP resources support gzip compression. ([\#13221](https://github.com/matrix-org/synapse/issues/13221))
|
||||
- Add steps describing how to elevate an existing user to administrator by manipulating the database. ([\#13230](https://github.com/matrix-org/synapse/issues/13230))
|
||||
- Fix wrong headline for `url_preview_accept_language` in documentation. ([\#13437](https://github.com/matrix-org/synapse/issues/13437))
|
||||
- Remove redundant 'Contents' section from the Configuration Manual. Contributed by @dklimpel. ([\#13438](https://github.com/matrix-org/synapse/issues/13438))
|
||||
- Update documentation for config setting `macaroon_secret_key`. ([\#13443](https://github.com/matrix-org/synapse/issues/13443))
|
||||
- Update outdated information on `sso_mapping_providers` documentation. ([\#13449](https://github.com/matrix-org/synapse/issues/13449))
|
||||
- Fix example code in module documentation of `password_auth_provider_callbacks`. ([\#13450](https://github.com/matrix-org/synapse/issues/13450))
|
||||
- Make the configuration for the cache clearer. ([\#13481](https://github.com/matrix-org/synapse/issues/13481))
|
||||
|
||||
|
||||
Internal Changes
|
||||
----------------
|
||||
|
||||
- Extend the release script to automatically push a new SyTest branch, rather than having that be a manual process. ([\#12978](https://github.com/matrix-org/synapse/issues/12978))
|
||||
- Make minor clarifications to the error messages given when we fail to join a room via any server. ([\#13160](https://github.com/matrix-org/synapse/issues/13160))
|
||||
- Enable Complement CI tests in the 'latest deps' test run. ([\#13213](https://github.com/matrix-org/synapse/issues/13213))
|
||||
- Fix long-standing bugged logic which was never hit in `get_pdu` asking every remote destination even after it finds an event. ([\#13346](https://github.com/matrix-org/synapse/issues/13346))
|
||||
- Faster room joins: avoid blocking when pulling events with partially missing prev events. ([\#13355](https://github.com/matrix-org/synapse/issues/13355))
|
||||
- Instrument `/messages` for understandable traces in Jaeger. ([\#13368](https://github.com/matrix-org/synapse/issues/13368))
|
||||
- Remove an unused argument to `get_relations_for_event`. ([\#13383](https://github.com/matrix-org/synapse/issues/13383))
|
||||
- Add a `merge-back` command to the release script, which automates merging the correct branches after a release. ([\#13393](https://github.com/matrix-org/synapse/issues/13393))
|
||||
- Adding missing type hints to tests. ([\#13397](https://github.com/matrix-org/synapse/issues/13397))
|
||||
- Faster Room Joins: don't leave a stuck room partial state flag if the join fails. ([\#13403](https://github.com/matrix-org/synapse/issues/13403))
|
||||
- Refactor `_resolve_state_at_missing_prevs` to compute an `EventContext` instead. ([\#13404](https://github.com/matrix-org/synapse/issues/13404), [\#13431](https://github.com/matrix-org/synapse/issues/13431))
|
||||
- Faster Room Joins: prevent Synapse from answering federated join requests for a room which it has not fully joined yet. ([\#13416](https://github.com/matrix-org/synapse/issues/13416))
|
||||
- Re-enable running Complement tests against Synapse with workers. ([\#13420](https://github.com/matrix-org/synapse/issues/13420))
|
||||
- Prevent unnecessary lookups to any external `get_event` cache. Contributed by Nick @ Beeper (@fizzadar). ([\#13435](https://github.com/matrix-org/synapse/issues/13435))
|
||||
- Add some tracing to give more insight into local room joins. ([\#13439](https://github.com/matrix-org/synapse/issues/13439))
|
||||
- Rename class `RateLimitConfig` to `RatelimitSettings` and `FederationRateLimitConfig` to `FederationRatelimitSettings`. ([\#13442](https://github.com/matrix-org/synapse/issues/13442))
|
||||
- Add some comments about how event push actions are stored. ([\#13445](https://github.com/matrix-org/synapse/issues/13445), [\#13455](https://github.com/matrix-org/synapse/issues/13455))
|
||||
- Improve rebuild speed for the "synapse-workers" docker image. ([\#13447](https://github.com/matrix-org/synapse/issues/13447))
|
||||
- Fix `@tag_args` being off-by-one with the arguments when tagging a span (tracing). ([\#13452](https://github.com/matrix-org/synapse/issues/13452))
|
||||
- Update type of `EventContext.rejected`. ([\#13460](https://github.com/matrix-org/synapse/issues/13460))
|
||||
- Use literals in place of `HTTPStatus` constants in tests. ([\#13463](https://github.com/matrix-org/synapse/issues/13463), [\#13469](https://github.com/matrix-org/synapse/issues/13469))
|
||||
- Correct a misnamed argument in state res v2 internals. ([\#13467](https://github.com/matrix-org/synapse/issues/13467))
|
||||
|
||||
|
||||
Synapse 1.64.0 (2022-08-02)
|
||||
===========================
|
||||
|
||||
No significant changes since 1.64.0rc2.
|
||||
|
||||
|
||||
Deprecation Warning
|
||||
-------------------
|
||||
|
||||
Synapse v1.66.0 will remove the ability to delegate the tasks of verifying email address ownership, and password reset confirmation, to an identity server.
|
||||
|
||||
If you require your homeserver to verify e-mail addresses or to support password resets via e-mail, please configure your homeserver with SMTP access so that it can send e-mails on its own behalf.
|
||||
[Consult the configuration documentation for more information.](https://matrix-org.github.io/synapse/latest/usage/configuration/config_documentation.html#email)
|
||||
|
||||
|
||||
Synapse 1.64.0rc2 (2022-07-29)
|
||||
==============================
|
||||
|
||||
This RC reintroduces support for `account_threepid_delegates.email`, which was removed in 1.64.0rc1. It remains deprecated and will be removed altogether in a future release. ([\#13406](https://github.com/matrix-org/synapse/issues/13406))
|
||||
This RC reintroduces support for `account_threepid_delegates.email`, which was removed in 1.64.0rc1. It remains deprecated and will be removed altogether in Synapse v1.66.0. ([\#13406](https://github.com/matrix-org/synapse/issues/13406))
|
||||
|
||||
|
||||
Synapse 1.64.0rc1 (2022-07-26)
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
Refactor the code that calculates the state events to send into new rooms.
|
||||
@@ -0,0 +1 @@
|
||||
Allow use of both `@trace` and `@tag_args` stacked on the same function (tracing).
|
||||
@@ -0,0 +1 @@
|
||||
Faster joins: update the rejected state of events during de-partial-stating.
|
||||
@@ -0,0 +1 @@
|
||||
Clean-up tests for notifications.
|
||||
@@ -0,0 +1 @@
|
||||
Add `openssl` example for generating registration HMAC digest.
|
||||
@@ -0,0 +1 @@
|
||||
Add some miscellaneous comments to document sync, especially around `compute_state_delta`.
|
||||
@@ -0,0 +1 @@
|
||||
Use literals in place of `HTTPStatus` constants in tests.
|
||||
@@ -0,0 +1 @@
|
||||
Add comments about how event push actions are rotated.
|
||||
@@ -0,0 +1 @@
|
||||
Use literals in place of `HTTPStatus` constants in tests.
|
||||
@@ -0,0 +1 @@
|
||||
Document that event purging related to the `redaction_retention_period` config option is executed only every 5 minutes.
|
||||
@@ -0,0 +1 @@
|
||||
Modify HTML template content to better support mobile devices' screen sizes.
|
||||
@@ -0,0 +1 @@
|
||||
Document that the `DOCKER_BUILDKIT=1` flag is needed to build the docker image.
|
||||
+2779
-1889
File diff suppressed because it is too large
Load Diff
Vendored
+18
@@ -1,3 +1,21 @@
|
||||
matrix-synapse-py3 (1.65.0~rc2) stable; urgency=medium
|
||||
|
||||
* New Synapse release 1.65.0rc2.
|
||||
|
||||
-- Synapse Packaging team <packages@matrix.org> Thu, 11 Aug 2022 11:38:18 +0100
|
||||
|
||||
matrix-synapse-py3 (1.65.0~rc1) stable; urgency=medium
|
||||
|
||||
* New Synapse release 1.65.0rc1.
|
||||
|
||||
-- Synapse Packaging team <packages@matrix.org> Tue, 09 Aug 2022 11:39:29 +0100
|
||||
|
||||
matrix-synapse-py3 (1.64.0) stable; urgency=medium
|
||||
|
||||
* New Synapse release 1.64.0.
|
||||
|
||||
-- Synapse Packaging team <packages@matrix.org> Tue, 02 Aug 2022 10:32:30 +0100
|
||||
|
||||
matrix-synapse-py3 (1.64.0~rc2) stable; urgency=medium
|
||||
|
||||
* New Synapse release 1.64.0rc2.
|
||||
|
||||
+24
-3
@@ -40,7 +40,8 @@ FROM docker.io/python:${PYTHON_VERSION}-slim as requirements
|
||||
RUN \
|
||||
--mount=type=cache,target=/var/cache/apt,sharing=locked \
|
||||
--mount=type=cache,target=/var/lib/apt,sharing=locked \
|
||||
apt-get update -qq && apt-get install -yqq git \
|
||||
apt-get update -qq && apt-get install -yqq \
|
||||
build-essential cargo git libffi-dev libssl-dev \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# We install poetry in its own build stage to avoid its dependencies conflicting with
|
||||
@@ -68,7 +69,18 @@ COPY pyproject.toml poetry.lock /synapse/
|
||||
# reason, such as when a git repository is used directly as a dependency.
|
||||
ARG TEST_ONLY_SKIP_DEP_HASH_VERIFICATION
|
||||
|
||||
RUN /root/.local/bin/poetry export --extras all -o /synapse/requirements.txt ${TEST_ONLY_SKIP_DEP_HASH_VERIFICATION:+--without-hashes}
|
||||
# If specified, we won't use the Poetry lockfile.
|
||||
# Instead, we'll just install what a regular `pip install` would from PyPI.
|
||||
ARG TEST_ONLY_IGNORE_POETRY_LOCKFILE
|
||||
|
||||
# Export the dependencies, but only if we're actually going to use the Poetry lockfile.
|
||||
# Otherwise, just create an empty requirements file so that the Dockerfile can
|
||||
# proceed.
|
||||
RUN if [ -z "$TEST_ONLY_IGNORE_POETRY_LOCKFILE" ]; then \
|
||||
/root/.local/bin/poetry export --extras all -o /synapse/requirements.txt ${TEST_ONLY_SKIP_DEP_HASH_VERIFICATION:+--without-hashes}; \
|
||||
else \
|
||||
touch /synapse/requirements.txt; \
|
||||
fi
|
||||
|
||||
###
|
||||
### Stage 1: builder
|
||||
@@ -108,8 +120,17 @@ COPY synapse /synapse/synapse/
|
||||
# ... and what we need to `pip install`.
|
||||
COPY pyproject.toml README.rst /synapse/
|
||||
|
||||
# Repeat of earlier build argument declaration, as this is a new build stage.
|
||||
ARG TEST_ONLY_IGNORE_POETRY_LOCKFILE
|
||||
|
||||
# Install the synapse package itself.
|
||||
RUN pip install --prefix="/install" --no-deps --no-warn-script-location /synapse
|
||||
# If we have populated requirements.txt, we don't install any dependencies
|
||||
# as we should already have those from the previous `pip install` step.
|
||||
RUN if [ -z "$TEST_ONLY_IGNORE_POETRY_LOCKFILE" ]; then \
|
||||
pip install --prefix="/install" --no-deps --no-warn-script-location /synapse[all]; \
|
||||
else \
|
||||
pip install --prefix="/install" --no-warn-script-location /synapse[all]; \
|
||||
fi
|
||||
|
||||
###
|
||||
### Stage 2: runtime
|
||||
|
||||
+51
-28
@@ -1,39 +1,62 @@
|
||||
# syntax=docker/dockerfile:1
|
||||
# Inherit from the official Synapse docker image
|
||||
|
||||
ARG SYNAPSE_VERSION=latest
|
||||
|
||||
# first of all, we create a base image with an nginx which we can copy into the
|
||||
# target image. For repeated rebuilds, this is much faster than apt installing
|
||||
# each time.
|
||||
|
||||
FROM debian:bullseye-slim AS deps_base
|
||||
RUN \
|
||||
--mount=type=cache,target=/var/cache/apt,sharing=locked \
|
||||
--mount=type=cache,target=/var/lib/apt,sharing=locked \
|
||||
apt-get update -qq && \
|
||||
DEBIAN_FRONTEND=noninteractive apt-get install -yqq --no-install-recommends \
|
||||
redis-server nginx-light
|
||||
|
||||
# Similarly, a base to copy the redis server from.
|
||||
#
|
||||
# The redis docker image has fewer dynamic libraries than the debian package,
|
||||
# which makes it much easier to copy (but we need to make sure we use an image
|
||||
# based on the same debian version as the synapse image, to make sure we get
|
||||
# the expected version of libc.
|
||||
FROM redis:6-bullseye AS redis_base
|
||||
|
||||
# now build the final image, based on the the regular Synapse docker image
|
||||
FROM matrixdotorg/synapse:$SYNAPSE_VERSION
|
||||
|
||||
# Install deps
|
||||
RUN \
|
||||
--mount=type=cache,target=/var/cache/apt,sharing=locked \
|
||||
--mount=type=cache,target=/var/lib/apt,sharing=locked \
|
||||
apt-get update -qq && \
|
||||
DEBIAN_FRONTEND=noninteractive apt-get install -yqq --no-install-recommends \
|
||||
redis-server nginx-light
|
||||
# Install supervisord with pip instead of apt, to avoid installing a second
|
||||
# copy of python.
|
||||
RUN --mount=type=cache,target=/root/.cache/pip \
|
||||
pip install supervisor~=4.2
|
||||
RUN mkdir -p /etc/supervisor/conf.d
|
||||
|
||||
# Install supervisord with pip instead of apt, to avoid installing a second
|
||||
# copy of python.
|
||||
RUN --mount=type=cache,target=/root/.cache/pip \
|
||||
pip install supervisor~=4.2
|
||||
# Copy over redis and nginx
|
||||
COPY --from=redis_base /usr/local/bin/redis-server /usr/local/bin
|
||||
|
||||
# Disable the default nginx sites
|
||||
RUN rm /etc/nginx/sites-enabled/default
|
||||
COPY --from=deps_base /usr/sbin/nginx /usr/sbin
|
||||
COPY --from=deps_base /usr/share/nginx /usr/share/nginx
|
||||
COPY --from=deps_base /usr/lib/nginx /usr/lib/nginx
|
||||
COPY --from=deps_base /etc/nginx /etc/nginx
|
||||
RUN rm /etc/nginx/sites-enabled/default
|
||||
RUN mkdir /var/log/nginx /var/lib/nginx
|
||||
RUN chown www-data /var/log/nginx /var/lib/nginx
|
||||
|
||||
# Copy Synapse worker, nginx and supervisord configuration template files
|
||||
COPY ./docker/conf-workers/* /conf/
|
||||
# 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/
|
||||
# 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
|
||||
# Expose nginx listener port
|
||||
EXPOSE 8080/tcp
|
||||
|
||||
# A script to read environment variables and create the necessary
|
||||
# files to run the desired worker configuration. Will start supervisord.
|
||||
COPY ./docker/configure_workers_and_start.py /configure_workers_and_start.py
|
||||
ENTRYPOINT ["/configure_workers_and_start.py"]
|
||||
# A script to read environment variables and create the necessary
|
||||
# files to run the desired worker configuration. Will start supervisord.
|
||||
COPY ./docker/configure_workers_and_start.py /configure_workers_and_start.py
|
||||
ENTRYPOINT ["/configure_workers_and_start.py"]
|
||||
|
||||
# Replace the healthcheck with one which checks *all* the workers. The script
|
||||
# is generated by configure_workers_and_start.py.
|
||||
HEALTHCHECK --start-period=5s --interval=15s --timeout=5s \
|
||||
CMD /bin/sh /healthcheck.sh
|
||||
# Replace the healthcheck with one which checks *all* the workers. The script
|
||||
# is generated by configure_workers_and_start.py.
|
||||
HEALTHCHECK --start-period=5s --interval=15s --timeout=5s \
|
||||
CMD /bin/sh /healthcheck.sh
|
||||
|
||||
+1
-1
@@ -191,7 +191,7 @@ If you need to build the image from a Synapse checkout, use the following `docke
|
||||
build` command from the repo's root:
|
||||
|
||||
```
|
||||
docker build -t matrixdotorg/synapse -f docker/Dockerfile .
|
||||
DOCKER_BUILDKIT=1 docker build -t matrixdotorg/synapse -f docker/Dockerfile .
|
||||
```
|
||||
|
||||
You can choose to build a different docker image by changing the value of the `-f` flag to
|
||||
|
||||
@@ -19,7 +19,7 @@ username=www-data
|
||||
autorestart=true
|
||||
|
||||
[program:redis]
|
||||
command=/usr/local/bin/prefix-log /usr/bin/redis-server /etc/redis/redis.conf --daemonize no
|
||||
command=/usr/local/bin/prefix-log /usr/local/bin/redis-server
|
||||
priority=1
|
||||
stdout_logfile=/dev/stdout
|
||||
stdout_logfile_maxbytes=0
|
||||
|
||||
@@ -46,7 +46,24 @@ As an example:
|
||||
The MAC is the hex digest output of the HMAC-SHA1 algorithm, with the key being
|
||||
the shared secret and the content being the nonce, user, password, either the
|
||||
string "admin" or "notadmin", and optionally the user_type
|
||||
each separated by NULs. For an example of generation in Python:
|
||||
each separated by NULs.
|
||||
|
||||
Here is an easy way to generate the HMAC digest if you have Bash and OpenSSL:
|
||||
|
||||
```bash
|
||||
# Update these values and then paste this code block into a bash terminal
|
||||
nonce='thisisanonce'
|
||||
username='pepper_roni'
|
||||
password='pizza'
|
||||
admin='admin'
|
||||
secret='shared_secret'
|
||||
|
||||
printf '%s\0%s\0%s\0%s' "$nonce" "$username" "$password" "$admin" |
|
||||
openssl sha1 -hmac "$secret" |
|
||||
awk '{print $2}'
|
||||
```
|
||||
|
||||
For an example of generation in Python:
|
||||
|
||||
```python
|
||||
import hmac, hashlib
|
||||
@@ -70,4 +87,4 @@ def generate_mac(nonce, user, password, admin=False, user_type=None):
|
||||
mac.update(user_type.encode('utf8'))
|
||||
|
||||
return mac.hexdigest()
|
||||
```
|
||||
```
|
||||
|
||||
@@ -263,7 +263,7 @@ class MyAuthProvider:
|
||||
return None
|
||||
|
||||
if self.credentials.get(username) == login_dict.get("my_field"):
|
||||
return self.api.get_qualified_user_id(username)
|
||||
return (self.api.get_qualified_user_id(username), None)
|
||||
|
||||
async def check_pass(
|
||||
self,
|
||||
@@ -280,5 +280,5 @@ class MyAuthProvider:
|
||||
return None
|
||||
|
||||
if self.credentials.get(username) == login_dict.get("password"):
|
||||
return self.api.get_qualified_user_id(username)
|
||||
return (self.api.get_qualified_user_id(username), None)
|
||||
```
|
||||
|
||||
@@ -22,7 +22,7 @@ choose their own username.
|
||||
In the first case - where users are automatically allocated a Matrix ID - it is
|
||||
the responsibility of the mapping provider to normalise the SSO attributes and
|
||||
map them to a valid Matrix ID. The [specification for Matrix
|
||||
IDs](https://matrix.org/docs/spec/appendices#user-identifiers) has some
|
||||
IDs](https://spec.matrix.org/latest/appendices/#user-identifiers) has some
|
||||
information about what is considered valid.
|
||||
|
||||
If the mapping provider does not assign a Matrix ID, then Synapse will
|
||||
@@ -37,9 +37,10 @@ as Synapse). The Synapse config is then modified to point to the mapping provide
|
||||
## OpenID Mapping Providers
|
||||
|
||||
The OpenID mapping provider can be customized by editing the
|
||||
`oidc_config.user_mapping_provider.module` config option.
|
||||
[`oidc_providers.user_mapping_provider.module`](usage/configuration/config_documentation.md#oidc_providers)
|
||||
config option.
|
||||
|
||||
`oidc_config.user_mapping_provider.config` allows you to provide custom
|
||||
`oidc_providers.user_mapping_provider.config` allows you to provide custom
|
||||
configuration options to the module. Check with the module's documentation for
|
||||
what options it provides (if any). The options listed by default are for the
|
||||
user mapping provider built in to Synapse. If using a custom module, you should
|
||||
@@ -58,7 +59,7 @@ A custom mapping provider must specify the following methods:
|
||||
- This method should have the `@staticmethod` decoration.
|
||||
- Arguments:
|
||||
- `config` - A `dict` representing the parsed content of the
|
||||
`oidc_config.user_mapping_provider.config` homeserver config option.
|
||||
`oidc_providers.user_mapping_provider.config` homeserver config option.
|
||||
Runs on homeserver startup. Providers should extract and validate
|
||||
any option values they need here.
|
||||
- Whatever is returned will be passed back to the user mapping provider module's
|
||||
@@ -102,7 +103,7 @@ A custom mapping provider must specify the following methods:
|
||||
will be returned as part of the response during a successful login.
|
||||
|
||||
Note that care should be taken to not overwrite any of the parameters
|
||||
usually returned as part of the [login response](https://matrix.org/docs/spec/client_server/latest#post-matrix-client-r0-login).
|
||||
usually returned as part of the [login response](https://spec.matrix.org/latest/client-server-api/#post_matrixclientv3login).
|
||||
|
||||
### Default OpenID Mapping Provider
|
||||
|
||||
@@ -113,7 +114,8 @@ specified in the config. It is located at
|
||||
## SAML Mapping Providers
|
||||
|
||||
The SAML mapping provider can be customized by editing the
|
||||
`saml2_config.user_mapping_provider.module` config option.
|
||||
[`saml2_config.user_mapping_provider.module`](docs/usage/configuration/config_documentation.md#saml2_config)
|
||||
config option.
|
||||
|
||||
`saml2_config.user_mapping_provider.config` allows you to provide custom
|
||||
configuration options to the module. Check with the module's documentation for
|
||||
|
||||
@@ -91,6 +91,16 @@ process, for example:
|
||||
|
||||
# Upgrading to v1.64.0
|
||||
|
||||
## Deprecation of the ability to delegate e-mail verification to identity servers
|
||||
|
||||
Synapse v1.66.0 will remove the ability to delegate the tasks of verifying email address ownership, and password reset confirmation, to an identity server.
|
||||
|
||||
If you require your homeserver to verify e-mail addresses or to support password resets via e-mail, please configure your homeserver with SMTP access so that it can send e-mails on its own behalf.
|
||||
[Consult the configuration documentation for more information.](https://matrix-org.github.io/synapse/latest/usage/configuration/config_documentation.html#email)
|
||||
|
||||
The option that will be removed is `account_threepid_delegates.email`.
|
||||
|
||||
|
||||
## Changes to the event replication streams
|
||||
|
||||
Synapse now includes a flag indicating if an event is an outlier when
|
||||
|
||||
@@ -5,8 +5,9 @@
|
||||
Many of the API calls in the admin api will require an `access_token` for a
|
||||
server admin. (Note that a server admin is distinct from a room admin.)
|
||||
|
||||
A user can be marked as a server admin by updating the database directly, e.g.:
|
||||
An existing user can be marked as a server admin by updating the database directly.
|
||||
|
||||
Check your [database settings](config_documentation.md#database) in the configuration file, connect to the correct database using either `psql [database name]` (if using PostgreSQL) or `sqlite3 path/to/your/database.db` (if using SQLite) and elevate the user `@foo:bar.com` to administrator.
|
||||
```sql
|
||||
UPDATE users SET admin = 1 WHERE name = '@foo:bar.com';
|
||||
```
|
||||
|
||||
@@ -2,11 +2,11 @@
|
||||
|
||||
This API allows you to manage tokens which can be used to authenticate
|
||||
registration requests, as proposed in
|
||||
[MSC3231](https://github.com/matrix-org/matrix-doc/blob/main/proposals/3231-token-authenticated-registration.md).
|
||||
[MSC3231](https://github.com/matrix-org/matrix-doc/blob/main/proposals/3231-token-authenticated-registration.md)
|
||||
and stabilised in version 1.2 of the Matrix specification.
|
||||
To use it, you will need to enable the `registration_requires_token` config
|
||||
option, and authenticate by providing an `access_token` for a server admin:
|
||||
see [Admin API](../../usage/administration/admin_api).
|
||||
Note that this API is still experimental; not all clients may support it yet.
|
||||
see [Admin API](../admin_api).
|
||||
|
||||
|
||||
## Registration token objects
|
||||
|
||||
@@ -72,49 +72,6 @@ apply if you want your config file to be read properly. A few helpful things to
|
||||
In addition, each setting has an example of its usage, with the proper indentation
|
||||
shown.
|
||||
|
||||
## Contents
|
||||
[Modules](#modules)
|
||||
|
||||
[Server](#server)
|
||||
|
||||
[Homeserver Blocking](#homeserver-blocking)
|
||||
|
||||
[TLS](#tls)
|
||||
|
||||
[Federation](#federation)
|
||||
|
||||
[Caching](#caching)
|
||||
|
||||
[Database](#database)
|
||||
|
||||
[Logging](#logging)
|
||||
|
||||
[Ratelimiting](#ratelimiting)
|
||||
|
||||
[Media Store](#media-store)
|
||||
|
||||
[Captcha](#captcha)
|
||||
|
||||
[TURN](#turn)
|
||||
|
||||
[Registration](#registration)
|
||||
|
||||
[API Configuration](#api-configuration)
|
||||
|
||||
[Signing Keys](#signing-keys)
|
||||
|
||||
[Single Sign On Integration](#single-sign-on-integration)
|
||||
|
||||
[Push](#push)
|
||||
|
||||
[Rooms](#rooms)
|
||||
|
||||
[Opentracing](#opentracing)
|
||||
|
||||
[Workers](#workers)
|
||||
|
||||
[Background Updates](#background-updates)
|
||||
|
||||
## Modules
|
||||
|
||||
Server admins can expand Synapse's functionality with external modules.
|
||||
@@ -486,7 +443,8 @@ Sub-options for each listener include:
|
||||
|
||||
* `names`: a list of names of HTTP resources. See below for a list of valid resource names.
|
||||
|
||||
* `compress`: set to true to enable HTTP compression for this resource.
|
||||
* `compress`: set to true to enable gzip compression on HTTP bodies for this resource. This is currently only supported with the
|
||||
`client`, `consent` and `metrics` resources.
|
||||
|
||||
* `additional_resources`: Only valid for an 'http' listener. A map of
|
||||
additional endpoints which should be loaded via dynamic modules.
|
||||
@@ -801,6 +759,10 @@ allowed_avatar_mimetypes: ["image/png", "image/jpeg", "image/gif"]
|
||||
How long to keep redacted events in unredacted form in the database. After
|
||||
this period redacted events get replaced with their redacted form in the DB.
|
||||
|
||||
Synapse will check whether the rentention period has concluded for redacted
|
||||
events every 5 minutes. Thus, even if this option is set to `0`, Synapse may
|
||||
still take up to 5 minutes to purge redacted events from the database.
|
||||
|
||||
Defaults to `7d`. Set to `null` to disable.
|
||||
|
||||
Example configuration:
|
||||
@@ -1098,26 +1060,26 @@ allow_device_name_lookup_over_federation: true
|
||||
---
|
||||
## Caching ##
|
||||
|
||||
Options related to caching
|
||||
Options related to caching.
|
||||
|
||||
---
|
||||
### `event_cache_size`
|
||||
|
||||
The number of events to cache in memory. Not affected by
|
||||
`caches.global_factor`. Defaults to 10K.
|
||||
`caches.global_factor` and is not part of the `caches` section. Defaults to 10K.
|
||||
|
||||
Example configuration:
|
||||
```yaml
|
||||
event_cache_size: 15K
|
||||
```
|
||||
---
|
||||
### `cache` and associated values
|
||||
### `caches` and associated values
|
||||
|
||||
A cache 'factor' is a multiplier that can be applied to each of
|
||||
Synapse's caches in order to increase or decrease the maximum
|
||||
number of entries that can be stored.
|
||||
|
||||
Caching can be configured through the following sub-options:
|
||||
`caches` can be configured through the following sub-options:
|
||||
|
||||
* `global_factor`: Controls the global cache factor, which is the default cache factor
|
||||
for all caches if a specific factor for that cache is not otherwise
|
||||
@@ -1179,6 +1141,7 @@ Caching can be configured through the following sub-options:
|
||||
|
||||
Example configuration:
|
||||
```yaml
|
||||
event_cache_size: 15K
|
||||
caches:
|
||||
global_factor: 1.0
|
||||
per_cache_factors:
|
||||
@@ -1858,7 +1821,7 @@ Example configuration:
|
||||
max_spider_size: 8M
|
||||
```
|
||||
---
|
||||
### `url_preview_language`
|
||||
### `url_preview_accept_language`
|
||||
|
||||
A list of values for the Accept-Language HTTP header used when
|
||||
downloading webpages during URL preview generation. This allows
|
||||
@@ -2537,9 +2500,13 @@ track_appservice_user_ips: true
|
||||
---
|
||||
### `macaroon_secret_key`
|
||||
|
||||
A secret which is used to sign access tokens. If none is specified,
|
||||
the `registration_shared_secret` is used, if one is given; otherwise,
|
||||
a secret key is derived from the signing key.
|
||||
A secret which is used to sign
|
||||
- access token for guest users,
|
||||
- short-term login token used during SSO logins (OIDC or SAML2) and
|
||||
- token used for unsubscribing from email notifications.
|
||||
|
||||
If none is specified, the `registration_shared_secret` is used, if one is given;
|
||||
otherwise, a secret key is derived from the signing key.
|
||||
|
||||
Example configuration:
|
||||
```yaml
|
||||
|
||||
Generated
+12
-12
@@ -177,7 +177,7 @@ optional = false
|
||||
python-versions = "*"
|
||||
|
||||
[package.extras]
|
||||
test = ["flake8 (==3.7.8)", "hypothesis (==3.55.3)"]
|
||||
test = ["hypothesis (==3.55.3)", "flake8 (==3.7.8)"]
|
||||
|
||||
[[package]]
|
||||
name = "constantly"
|
||||
@@ -435,8 +435,8 @@ optional = false
|
||||
python-versions = ">=3.6"
|
||||
|
||||
[package.extras]
|
||||
test = ["pytest", "pytest-trio", "pytest-asyncio", "testpath", "trio", "async-timeout"]
|
||||
trio = ["trio", "async-generator"]
|
||||
trio = ["async-generator", "trio"]
|
||||
test = ["async-timeout", "trio", "testpath", "pytest-asyncio", "pytest-trio", "pytest"]
|
||||
|
||||
[[package]]
|
||||
name = "jinja2"
|
||||
@@ -535,12 +535,12 @@ attrs = "*"
|
||||
importlib-metadata = {version = ">=1.4", markers = "python_version < \"3.8\""}
|
||||
|
||||
[package.extras]
|
||||
dev = ["tox", "twisted", "aiounittest", "mypy (==0.910)", "black (==22.3.0)", "flake8 (==4.0.1)", "isort (==5.9.3)", "build (==0.8.0)", "twine (==4.0.1)"]
|
||||
test = ["tox", "twisted", "aiounittest"]
|
||||
test = ["aiounittest", "twisted", "tox"]
|
||||
dev = ["twine (==4.0.1)", "build (==0.8.0)", "isort (==5.9.3)", "flake8 (==4.0.1)", "black (==22.3.0)", "mypy (==0.910)", "aiounittest", "twisted", "tox"]
|
||||
|
||||
[[package]]
|
||||
name = "matrix-synapse-ldap3"
|
||||
version = "0.2.1"
|
||||
version = "0.2.2"
|
||||
description = "An LDAP3 auth provider for Synapse"
|
||||
category = "main"
|
||||
optional = true
|
||||
@@ -552,7 +552,7 @@ service-identity = "*"
|
||||
Twisted = ">=15.1.0"
|
||||
|
||||
[package.extras]
|
||||
dev = ["matrix-synapse", "tox", "ldaptor", "mypy (==0.910)", "types-setuptools", "black (==22.3.0)", "flake8 (==4.0.1)", "isort (==5.9.3)"]
|
||||
dev = ["isort (==5.9.3)", "flake8 (==4.0.1)", "black (==22.3.0)", "types-setuptools", "mypy (==0.910)", "ldaptor", "tox", "matrix-synapse"]
|
||||
|
||||
[[package]]
|
||||
name = "mccabe"
|
||||
@@ -820,10 +820,10 @@ optional = false
|
||||
python-versions = ">=3.6"
|
||||
|
||||
[package.extras]
|
||||
tests = ["coverage[toml] (==5.0.4)", "pytest (>=6.0.0,<7.0.0)"]
|
||||
docs = ["zope.interface", "sphinx-rtd-theme", "sphinx"]
|
||||
dev = ["pre-commit", "mypy", "coverage[toml] (==5.0.4)", "pytest (>=6.0.0,<7.0.0)", "cryptography (>=3.3.1)", "zope.interface", "sphinx-rtd-theme", "sphinx"]
|
||||
crypto = ["cryptography (>=3.3.1)"]
|
||||
dev = ["sphinx", "sphinx-rtd-theme", "zope.interface", "cryptography (>=3.3.1)", "pytest (>=6.0.0,<7.0.0)", "coverage[toml] (==5.0.4)", "mypy", "pre-commit"]
|
||||
docs = ["sphinx", "sphinx-rtd-theme", "zope.interface"]
|
||||
tests = ["pytest (>=6.0.0,<7.0.0)", "coverage[toml] (==5.0.4)"]
|
||||
|
||||
[[package]]
|
||||
name = "pymacaroons"
|
||||
@@ -2055,8 +2055,8 @@ matrix-common = [
|
||||
{file = "matrix_common-1.2.1.tar.gz", hash = "sha256:a99dcf02a6bd95b24a5a61b354888a2ac92bf2b4b839c727b8dd9da2cdfa3853"},
|
||||
]
|
||||
matrix-synapse-ldap3 = [
|
||||
{file = "matrix-synapse-ldap3-0.2.1.tar.gz", hash = "sha256:bfb4390f4a262ffb0d6f057ff3aeb1e46d4e52ff420a064d795fb4f555f00285"},
|
||||
{file = "matrix_synapse_ldap3-0.2.1-py3-none-any.whl", hash = "sha256:1b3310a60f1d06466f35905a269b6df95747fd1305f2b7fe638f373963b2aa2c"},
|
||||
{file = "matrix-synapse-ldap3-0.2.2.tar.gz", hash = "sha256:b388d95693486eef69adaefd0fd9e84463d52fe17b0214a00efcaa669b73cb74"},
|
||||
{file = "matrix_synapse_ldap3-0.2.2-py3-none-any.whl", hash = "sha256:66ee4c85d7952c6c27fd04c09cdfdf4847b8e8b7d6a7ada6ba1100013bda060f"},
|
||||
]
|
||||
mccabe = [
|
||||
{file = "mccabe-0.6.1-py2.py3-none-any.whl", hash = "sha256:ab8a6258860da4b6677da4bd2fe5dc2c659cff31b3ee4f7f5d64e79735b80d42"},
|
||||
|
||||
+1
-1
@@ -54,7 +54,7 @@ skip_gitignore = true
|
||||
|
||||
[tool.poetry]
|
||||
name = "matrix-synapse"
|
||||
version = "1.64.0rc2"
|
||||
version = "1.65.0rc2"
|
||||
description = "Homeserver for the Matrix decentralised comms protocol"
|
||||
authors = ["Matrix.org Team and Contributors <packages@matrix.org>"]
|
||||
license = "Apache-2.0"
|
||||
|
||||
@@ -101,6 +101,7 @@ if [ -z "$skip_docker_build" ]; then
|
||||
echo_if_github "::group::Build Docker image: matrixdotorg/synapse"
|
||||
docker build -t matrixdotorg/synapse \
|
||||
--build-arg TEST_ONLY_SKIP_DEP_HASH_VERIFICATION \
|
||||
--build-arg TEST_ONLY_IGNORE_POETRY_LOCKFILE \
|
||||
-f "docker/Dockerfile" .
|
||||
echo_if_github "::endgroup::"
|
||||
|
||||
|
||||
+130
-32
@@ -32,6 +32,7 @@ import click
|
||||
import commonmark
|
||||
import git
|
||||
from click.exceptions import ClickException
|
||||
from git import GitCommandError, Repo
|
||||
from github import Github
|
||||
from packaging import version
|
||||
|
||||
@@ -55,9 +56,12 @@ def run_until_successful(
|
||||
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:
|
||||
Requirements:
|
||||
- The dev dependencies be installed, which can be done via:
|
||||
|
||||
pip install -e .[dev]
|
||||
pip install -e .[dev]
|
||||
|
||||
- A checkout of the sytest repository at ../sytest
|
||||
|
||||
Then to use:
|
||||
|
||||
@@ -75,6 +79,8 @@ def cli() -> None:
|
||||
|
||||
# Optional: generate some nice links for the announcement
|
||||
|
||||
./scripts-dev/release.py merge-back
|
||||
|
||||
./scripts-dev/release.py announce
|
||||
|
||||
If the env var GH_TOKEN (or GITHUB_TOKEN) is set, or passed into the
|
||||
@@ -89,10 +95,12 @@ def prepare() -> None:
|
||||
"""
|
||||
|
||||
# Make sure we're in a git repo.
|
||||
repo = get_repo_and_check_clean_checkout()
|
||||
synapse_repo = get_repo_and_check_clean_checkout()
|
||||
sytest_repo = get_repo_and_check_clean_checkout("../sytest", "sytest")
|
||||
|
||||
click.secho("Updating git repo...")
|
||||
repo.remote().fetch()
|
||||
click.secho("Updating Synapse and Sytest git repos...")
|
||||
synapse_repo.remote().fetch()
|
||||
sytest_repo.remote().fetch()
|
||||
|
||||
# Get the current version and AST from root Synapse module.
|
||||
current_version = get_package_version()
|
||||
@@ -166,12 +174,12 @@ def prepare() -> None:
|
||||
assert not parsed_new_version.is_postrelease
|
||||
|
||||
release_branch_name = get_release_branch_name(parsed_new_version)
|
||||
release_branch = find_ref(repo, release_branch_name)
|
||||
release_branch = find_ref(synapse_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)
|
||||
synapse_repo.git.checkout(release_branch_name)
|
||||
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
|
||||
@@ -187,25 +195,34 @@ def prepare() -> None:
|
||||
"Which branch should the release be based on?", default=default
|
||||
)
|
||||
|
||||
base_branch = find_ref(repo, branch_name)
|
||||
if not base_branch:
|
||||
print(f"Could not find base branch {branch_name}!")
|
||||
click.get_current_context().abort()
|
||||
for repo_name, repo in {"synapse": synapse_repo, "sytest": sytest_repo}.items():
|
||||
base_branch = find_ref(repo, branch_name)
|
||||
if not base_branch:
|
||||
print(f"Could not find base branch {branch_name} for {repo_name}!")
|
||||
click.get_current_context().abort()
|
||||
|
||||
# Check out the base branch and ensure it's up to date
|
||||
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)
|
||||
# Check out the base branch and ensure it's up to date
|
||||
repo.head.set_reference(
|
||||
base_branch, f"check out the base branch for {repo_name}"
|
||||
)
|
||||
repo.head.reset(index=True, working_tree=True)
|
||||
if not base_branch.is_remote():
|
||||
update_branch(repo)
|
||||
|
||||
# Create the new release 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]
|
||||
# Create the new release 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]
|
||||
|
||||
# Special-case SyTest: we don't actually prepare any files so we may
|
||||
# as well push it now (and only when we create a release branch;
|
||||
# not on subsequent RCs or full releases).
|
||||
if click.confirm("Push new SyTest branch?", default=True):
|
||||
sytest_repo.git.push("-u", sytest_repo.remote().name, release_branch_name)
|
||||
|
||||
# Switch to the release branch and ensure it's up to date.
|
||||
repo.git.checkout(release_branch_name)
|
||||
update_branch(repo)
|
||||
synapse_repo.git.checkout(release_branch_name)
|
||||
update_branch(synapse_repo)
|
||||
|
||||
# Update the version specified in pyproject.toml.
|
||||
subprocess.check_output(["poetry", "version", new_version])
|
||||
@@ -230,15 +247,15 @@ def prepare() -> None:
|
||||
run_until_successful('dch -M -r -D stable ""', shell=True)
|
||||
|
||||
# Show the user the changes and ask if they want to edit the change log.
|
||||
repo.git.add("-u")
|
||||
synapse_repo.git.add("-u")
|
||||
subprocess.run("git diff --cached", shell=True)
|
||||
|
||||
if click.confirm("Edit changelog?", default=False):
|
||||
click.edit(filename="CHANGES.md")
|
||||
|
||||
# Commit the changes.
|
||||
repo.git.add("-u")
|
||||
repo.git.commit("-m", new_version)
|
||||
synapse_repo.git.add("-u")
|
||||
synapse_repo.git.commit("-m", new_version)
|
||||
|
||||
# We give the option to bail here in case the user wants to make sure things
|
||||
# are OK before pushing.
|
||||
@@ -246,17 +263,21 @@ def prepare() -> None:
|
||||
print("")
|
||||
print("Run when ready to push:")
|
||||
print("")
|
||||
print(f"\tgit push -u {repo.remote().name} {repo.active_branch.name}")
|
||||
print(
|
||||
f"\tgit push -u {synapse_repo.remote().name} {synapse_repo.active_branch.name}"
|
||||
)
|
||||
print("")
|
||||
sys.exit(0)
|
||||
|
||||
# Otherwise, push and open the changelog in the browser.
|
||||
repo.git.push("-u", repo.remote().name, repo.active_branch.name)
|
||||
synapse_repo.git.push(
|
||||
"-u", synapse_repo.remote().name, synapse_repo.active_branch.name
|
||||
)
|
||||
|
||||
print("Opening the changelog in your browser...")
|
||||
print("Please ask others to give it a check.")
|
||||
click.launch(
|
||||
f"https://github.com/matrix-org/synapse/blob/{repo.active_branch.name}/CHANGES.md"
|
||||
f"https://github.com/matrix-org/synapse/blob/{synapse_repo.active_branch.name}/CHANGES.md"
|
||||
)
|
||||
|
||||
|
||||
@@ -423,6 +444,79 @@ def upload() -> None:
|
||||
)
|
||||
|
||||
|
||||
def _merge_into(repo: Repo, source: str, target: str) -> None:
|
||||
"""
|
||||
Merges branch `source` into branch `target`.
|
||||
Pulls both before merging and pushes the result.
|
||||
"""
|
||||
|
||||
# Update our branches and switch to the target branch
|
||||
for branch in [source, target]:
|
||||
click.echo(f"Switching to {branch} and pulling...")
|
||||
repo.heads[branch].checkout()
|
||||
# Pull so we're up to date
|
||||
repo.remote().pull()
|
||||
|
||||
assert repo.active_branch.name == target
|
||||
|
||||
try:
|
||||
# TODO This seemed easier than using GitPython directly
|
||||
click.echo(f"Merging {source}...")
|
||||
repo.git.merge(source)
|
||||
except GitCommandError as exc:
|
||||
# If a merge conflict occurs, give some context and try to
|
||||
# make it easy to abort if necessary.
|
||||
click.echo(exc)
|
||||
if not click.confirm(
|
||||
f"Likely merge conflict whilst merging ({source} → {target}). "
|
||||
f"Have you resolved it?"
|
||||
):
|
||||
repo.git.merge("--abort")
|
||||
return
|
||||
|
||||
# Push result.
|
||||
click.echo("Pushing...")
|
||||
repo.remote().push()
|
||||
|
||||
|
||||
@cli.command()
|
||||
def merge_back() -> None:
|
||||
"""Merge the release branch back into the appropriate branches.
|
||||
All branches will be automatically pulled from the remote and the results
|
||||
will be pushed to the remote."""
|
||||
|
||||
synapse_repo = get_repo_and_check_clean_checkout()
|
||||
branch_name = synapse_repo.active_branch.name
|
||||
|
||||
if not branch_name.startswith("release-v"):
|
||||
raise RuntimeError("Not on a release branch. This does not seem sensible.")
|
||||
|
||||
# Pull so we're up to date
|
||||
synapse_repo.remote().pull()
|
||||
|
||||
current_version = get_package_version()
|
||||
|
||||
if current_version.is_prerelease:
|
||||
# Release candidate
|
||||
if click.confirm(f"Merge {branch_name} → develop?", default=True):
|
||||
_merge_into(synapse_repo, branch_name, "develop")
|
||||
else:
|
||||
# Full release
|
||||
sytest_repo = get_repo_and_check_clean_checkout("../sytest", "sytest")
|
||||
|
||||
if click.confirm(f"Merge {branch_name} → master?", default=True):
|
||||
_merge_into(synapse_repo, branch_name, "master")
|
||||
|
||||
if click.confirm("Merge master → develop?", default=True):
|
||||
_merge_into(synapse_repo, "master", "develop")
|
||||
|
||||
if click.confirm(f"On SyTest, merge {branch_name} → master?", default=True):
|
||||
_merge_into(sytest_repo, branch_name, "master")
|
||||
|
||||
if click.confirm("On SyTest, merge master → develop?", default=True):
|
||||
_merge_into(sytest_repo, "master", "develop")
|
||||
|
||||
|
||||
@cli.command()
|
||||
def announce() -> None:
|
||||
"""Generate markdown to announce the release."""
|
||||
@@ -469,14 +563,18 @@ 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:
|
||||
def get_repo_and_check_clean_checkout(
|
||||
path: str = ".", name: str = "synapse"
|
||||
) -> git.Repo:
|
||||
"""Get the project repo and check it's not got any uncommitted changes."""
|
||||
try:
|
||||
repo = git.Repo()
|
||||
repo = git.Repo(path=path)
|
||||
except git.InvalidGitRepositoryError:
|
||||
raise click.ClickException("Not in Synapse repo.")
|
||||
raise click.ClickException(
|
||||
f"{path} is not a git repository (expecting a {name} repository)."
|
||||
)
|
||||
if repo.is_dirty():
|
||||
raise click.ClickException("Uncommitted changes exist.")
|
||||
raise click.ClickException(f"Uncommitted changes exist in {path}.")
|
||||
return repo
|
||||
|
||||
|
||||
|
||||
+15
-4
@@ -26,11 +26,17 @@ from synapse.api.errors import (
|
||||
Codes,
|
||||
InvalidClientTokenError,
|
||||
MissingClientTokenError,
|
||||
UnstableSpecAuthError,
|
||||
)
|
||||
from synapse.appservice import ApplicationService
|
||||
from synapse.http import get_request_user_agent
|
||||
from synapse.http.site import SynapseRequest
|
||||
from synapse.logging.opentracing import active_span, force_tracing, start_active_span
|
||||
from synapse.logging.opentracing import (
|
||||
active_span,
|
||||
force_tracing,
|
||||
start_active_span,
|
||||
trace,
|
||||
)
|
||||
from synapse.storage.databases.main.registration import TokenLookupResult
|
||||
from synapse.types import Requester, UserID, create_requester
|
||||
|
||||
@@ -106,8 +112,11 @@ class Auth:
|
||||
forgot = await self.store.did_forget(user_id, room_id)
|
||||
if not forgot:
|
||||
return membership, member_event_id
|
||||
|
||||
raise AuthError(403, "User %s not in room %s" % (user_id, room_id))
|
||||
raise UnstableSpecAuthError(
|
||||
403,
|
||||
"User %s not in room %s" % (user_id, room_id),
|
||||
errcode=Codes.NOT_JOINED,
|
||||
)
|
||||
|
||||
async def get_user_by_req(
|
||||
self,
|
||||
@@ -563,6 +572,7 @@ class Auth:
|
||||
|
||||
return query_params[0].decode("ascii")
|
||||
|
||||
@trace
|
||||
async def check_user_in_room_or_world_readable(
|
||||
self, room_id: str, user_id: str, allow_departed_users: bool = False
|
||||
) -> Tuple[str, Optional[str]]:
|
||||
@@ -600,8 +610,9 @@ class Auth:
|
||||
== HistoryVisibility.WORLD_READABLE
|
||||
):
|
||||
return Membership.JOIN, None
|
||||
raise AuthError(
|
||||
raise UnstableSpecAuthError(
|
||||
403,
|
||||
"User %s not in room %s, and room previews are disabled"
|
||||
% (user_id, room_id),
|
||||
errcode=Codes.NOT_JOINED,
|
||||
)
|
||||
|
||||
@@ -257,7 +257,8 @@ class GuestAccess:
|
||||
|
||||
class ReceiptTypes:
|
||||
READ: Final = "m.read"
|
||||
READ_PRIVATE: Final = "org.matrix.msc2285.read.private"
|
||||
READ_PRIVATE: Final = "m.read.private"
|
||||
UNSTABLE_READ_PRIVATE: Final = "org.matrix.msc2285.read.private"
|
||||
FULLY_READ: Final = "m.fully_read"
|
||||
|
||||
|
||||
@@ -268,4 +269,4 @@ class PublicRoomsFilterFields:
|
||||
"""
|
||||
|
||||
GENERIC_SEARCH_TERM: Final = "generic_search_term"
|
||||
ROOM_TYPES: Final = "org.matrix.msc3827.room_types"
|
||||
ROOM_TYPES: Final = "room_types"
|
||||
|
||||
+48
-10
@@ -26,6 +26,7 @@ from twisted.web import http
|
||||
from synapse.util import json_decoder
|
||||
|
||||
if typing.TYPE_CHECKING:
|
||||
from synapse.config.homeserver import HomeServerConfig
|
||||
from synapse.types import JsonDict
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
@@ -80,6 +81,12 @@ class Codes(str, Enum):
|
||||
INVALID_SIGNATURE = "M_INVALID_SIGNATURE"
|
||||
USER_DEACTIVATED = "M_USER_DEACTIVATED"
|
||||
|
||||
# Part of MSC3848
|
||||
# https://github.com/matrix-org/matrix-spec-proposals/pull/3848
|
||||
ALREADY_JOINED = "ORG.MATRIX.MSC3848.ALREADY_JOINED"
|
||||
NOT_JOINED = "ORG.MATRIX.MSC3848.NOT_JOINED"
|
||||
INSUFFICIENT_POWER = "ORG.MATRIX.MSC3848.INSUFFICIENT_POWER"
|
||||
|
||||
# The account has been suspended on the server.
|
||||
# By opposition to `USER_DEACTIVATED`, this is a reversible measure
|
||||
# that can possibly be appealed and reverted.
|
||||
@@ -167,7 +174,7 @@ class SynapseError(CodeMessageException):
|
||||
else:
|
||||
self._additional_fields = dict(additional_fields)
|
||||
|
||||
def error_dict(self) -> "JsonDict":
|
||||
def error_dict(self, config: Optional["HomeServerConfig"]) -> "JsonDict":
|
||||
return cs_error(self.msg, self.errcode, **self._additional_fields)
|
||||
|
||||
|
||||
@@ -213,7 +220,7 @@ class ConsentNotGivenError(SynapseError):
|
||||
)
|
||||
self._consent_uri = consent_uri
|
||||
|
||||
def error_dict(self) -> "JsonDict":
|
||||
def error_dict(self, config: Optional["HomeServerConfig"]) -> "JsonDict":
|
||||
return cs_error(self.msg, self.errcode, consent_uri=self._consent_uri)
|
||||
|
||||
|
||||
@@ -307,6 +314,37 @@ class AuthError(SynapseError):
|
||||
super().__init__(code, msg, errcode, additional_fields)
|
||||
|
||||
|
||||
class UnstableSpecAuthError(AuthError):
|
||||
"""An error raised when a new error code is being proposed to replace a previous one.
|
||||
This error will return a "org.matrix.unstable.errcode" property with the new error code,
|
||||
with the previous error code still being defined in the "errcode" property.
|
||||
|
||||
This error will include `org.matrix.msc3848.unstable.errcode` in the C-S error body.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
code: int,
|
||||
msg: str,
|
||||
errcode: str,
|
||||
previous_errcode: str = Codes.FORBIDDEN,
|
||||
additional_fields: Optional[dict] = None,
|
||||
):
|
||||
self.previous_errcode = previous_errcode
|
||||
super().__init__(code, msg, errcode, additional_fields)
|
||||
|
||||
def error_dict(self, config: Optional["HomeServerConfig"]) -> "JsonDict":
|
||||
fields = {}
|
||||
if config is not None and config.experimental.msc3848_enabled:
|
||||
fields["org.matrix.msc3848.unstable.errcode"] = self.errcode
|
||||
return cs_error(
|
||||
self.msg,
|
||||
self.previous_errcode,
|
||||
**fields,
|
||||
**self._additional_fields,
|
||||
)
|
||||
|
||||
|
||||
class InvalidClientCredentialsError(SynapseError):
|
||||
"""An error raised when there was a problem with the authorisation credentials
|
||||
in a client request.
|
||||
@@ -338,8 +376,8 @@ class InvalidClientTokenError(InvalidClientCredentialsError):
|
||||
super().__init__(msg=msg, errcode="M_UNKNOWN_TOKEN")
|
||||
self._soft_logout = soft_logout
|
||||
|
||||
def error_dict(self) -> "JsonDict":
|
||||
d = super().error_dict()
|
||||
def error_dict(self, config: Optional["HomeServerConfig"]) -> "JsonDict":
|
||||
d = super().error_dict(config)
|
||||
d["soft_logout"] = self._soft_logout
|
||||
return d
|
||||
|
||||
@@ -362,7 +400,7 @@ class ResourceLimitError(SynapseError):
|
||||
self.limit_type = limit_type
|
||||
super().__init__(code, msg, errcode=errcode)
|
||||
|
||||
def error_dict(self) -> "JsonDict":
|
||||
def error_dict(self, config: Optional["HomeServerConfig"]) -> "JsonDict":
|
||||
return cs_error(
|
||||
self.msg,
|
||||
self.errcode,
|
||||
@@ -397,7 +435,7 @@ class InvalidCaptchaError(SynapseError):
|
||||
super().__init__(code, msg, errcode)
|
||||
self.error_url = error_url
|
||||
|
||||
def error_dict(self) -> "JsonDict":
|
||||
def error_dict(self, config: Optional["HomeServerConfig"]) -> "JsonDict":
|
||||
return cs_error(self.msg, self.errcode, error_url=self.error_url)
|
||||
|
||||
|
||||
@@ -414,7 +452,7 @@ class LimitExceededError(SynapseError):
|
||||
super().__init__(code, msg, errcode)
|
||||
self.retry_after_ms = retry_after_ms
|
||||
|
||||
def error_dict(self) -> "JsonDict":
|
||||
def error_dict(self, config: Optional["HomeServerConfig"]) -> "JsonDict":
|
||||
return cs_error(self.msg, self.errcode, retry_after_ms=self.retry_after_ms)
|
||||
|
||||
|
||||
@@ -429,7 +467,7 @@ class RoomKeysVersionError(SynapseError):
|
||||
super().__init__(403, "Wrong room_keys version", Codes.WRONG_ROOM_KEYS_VERSION)
|
||||
self.current_version = current_version
|
||||
|
||||
def error_dict(self) -> "JsonDict":
|
||||
def error_dict(self, config: Optional["HomeServerConfig"]) -> "JsonDict":
|
||||
return cs_error(self.msg, self.errcode, current_version=self.current_version)
|
||||
|
||||
|
||||
@@ -469,7 +507,7 @@ class IncompatibleRoomVersionError(SynapseError):
|
||||
|
||||
self._room_version = room_version
|
||||
|
||||
def error_dict(self) -> "JsonDict":
|
||||
def error_dict(self, config: Optional["HomeServerConfig"]) -> "JsonDict":
|
||||
return cs_error(self.msg, self.errcode, room_version=self._room_version)
|
||||
|
||||
|
||||
@@ -515,7 +553,7 @@ class UnredactedContentDeletedError(SynapseError):
|
||||
)
|
||||
self.content_keep_ms = content_keep_ms
|
||||
|
||||
def error_dict(self) -> "JsonDict":
|
||||
def error_dict(self, config: Optional["HomeServerConfig"]) -> "JsonDict":
|
||||
extra = {}
|
||||
if self.content_keep_ms is not None:
|
||||
extra = {"fi.mau.msc2815.content_keep_ms": self.content_keep_ms}
|
||||
|
||||
@@ -17,7 +17,7 @@ from collections import OrderedDict
|
||||
from typing import Hashable, Optional, Tuple
|
||||
|
||||
from synapse.api.errors import LimitExceededError
|
||||
from synapse.config.ratelimiting import RateLimitConfig
|
||||
from synapse.config.ratelimiting import RatelimitSettings
|
||||
from synapse.storage.databases.main import DataStore
|
||||
from synapse.types import Requester
|
||||
from synapse.util import Clock
|
||||
@@ -314,8 +314,8 @@ class RequestRatelimiter:
|
||||
self,
|
||||
store: DataStore,
|
||||
clock: Clock,
|
||||
rc_message: RateLimitConfig,
|
||||
rc_admin_redaction: Optional[RateLimitConfig],
|
||||
rc_message: RatelimitSettings,
|
||||
rc_admin_redaction: Optional[RatelimitSettings],
|
||||
):
|
||||
self.store = store
|
||||
self.clock = clock
|
||||
|
||||
@@ -32,7 +32,7 @@ class ExperimentalConfig(Config):
|
||||
# MSC2716 (importing historical messages)
|
||||
self.msc2716_enabled: bool = experimental.get("msc2716_enabled", False)
|
||||
|
||||
# MSC2285 (private read receipts)
|
||||
# MSC2285 (unstable private read receipts)
|
||||
self.msc2285_enabled: bool = experimental.get("msc2285_enabled", False)
|
||||
|
||||
# MSC3244 (room version capabilities)
|
||||
@@ -88,5 +88,5 @@ class ExperimentalConfig(Config):
|
||||
# MSC3715: dir param on /relations.
|
||||
self.msc3715_enabled: bool = experimental.get("msc3715_enabled", False)
|
||||
|
||||
# MSC3827: Filtering of /publicRooms by room type
|
||||
self.msc3827_enabled: bool = experimental.get("msc3827_enabled", False)
|
||||
# MSC3848: Introduce errcodes for specific event sending failures
|
||||
self.msc3848_enabled: bool = experimental.get("msc3848_enabled", False)
|
||||
|
||||
@@ -21,7 +21,7 @@ from synapse.types import JsonDict
|
||||
from ._base import Config
|
||||
|
||||
|
||||
class RateLimitConfig:
|
||||
class RatelimitSettings:
|
||||
def __init__(
|
||||
self,
|
||||
config: Dict[str, float],
|
||||
@@ -34,7 +34,7 @@ class RateLimitConfig:
|
||||
|
||||
|
||||
@attr.s(auto_attribs=True)
|
||||
class FederationRateLimitConfig:
|
||||
class FederationRatelimitSettings:
|
||||
window_size: int = 1000
|
||||
sleep_limit: int = 10
|
||||
sleep_delay: int = 500
|
||||
@@ -50,11 +50,11 @@ class RatelimitConfig(Config):
|
||||
# Load the new-style messages config if it exists. Otherwise fall back
|
||||
# to the old method.
|
||||
if "rc_message" in config:
|
||||
self.rc_message = RateLimitConfig(
|
||||
self.rc_message = RatelimitSettings(
|
||||
config["rc_message"], defaults={"per_second": 0.2, "burst_count": 10.0}
|
||||
)
|
||||
else:
|
||||
self.rc_message = RateLimitConfig(
|
||||
self.rc_message = RatelimitSettings(
|
||||
{
|
||||
"per_second": config.get("rc_messages_per_second", 0.2),
|
||||
"burst_count": config.get("rc_message_burst_count", 10.0),
|
||||
@@ -64,9 +64,9 @@ class RatelimitConfig(Config):
|
||||
# Load the new-style federation config, if it exists. Otherwise, fall
|
||||
# back to the old method.
|
||||
if "rc_federation" in config:
|
||||
self.rc_federation = FederationRateLimitConfig(**config["rc_federation"])
|
||||
self.rc_federation = FederationRatelimitSettings(**config["rc_federation"])
|
||||
else:
|
||||
self.rc_federation = FederationRateLimitConfig(
|
||||
self.rc_federation = FederationRatelimitSettings(
|
||||
**{
|
||||
k: v
|
||||
for k, v in {
|
||||
@@ -80,17 +80,17 @@ class RatelimitConfig(Config):
|
||||
}
|
||||
)
|
||||
|
||||
self.rc_registration = RateLimitConfig(config.get("rc_registration", {}))
|
||||
self.rc_registration = RatelimitSettings(config.get("rc_registration", {}))
|
||||
|
||||
self.rc_registration_token_validity = RateLimitConfig(
|
||||
self.rc_registration_token_validity = RatelimitSettings(
|
||||
config.get("rc_registration_token_validity", {}),
|
||||
defaults={"per_second": 0.1, "burst_count": 5},
|
||||
)
|
||||
|
||||
rc_login_config = config.get("rc_login", {})
|
||||
self.rc_login_address = RateLimitConfig(rc_login_config.get("address", {}))
|
||||
self.rc_login_account = RateLimitConfig(rc_login_config.get("account", {}))
|
||||
self.rc_login_failed_attempts = RateLimitConfig(
|
||||
self.rc_login_address = RatelimitSettings(rc_login_config.get("address", {}))
|
||||
self.rc_login_account = RatelimitSettings(rc_login_config.get("account", {}))
|
||||
self.rc_login_failed_attempts = RatelimitSettings(
|
||||
rc_login_config.get("failed_attempts", {})
|
||||
)
|
||||
|
||||
@@ -101,20 +101,20 @@ class RatelimitConfig(Config):
|
||||
rc_admin_redaction = config.get("rc_admin_redaction")
|
||||
self.rc_admin_redaction = None
|
||||
if rc_admin_redaction:
|
||||
self.rc_admin_redaction = RateLimitConfig(rc_admin_redaction)
|
||||
self.rc_admin_redaction = RatelimitSettings(rc_admin_redaction)
|
||||
|
||||
self.rc_joins_local = RateLimitConfig(
|
||||
self.rc_joins_local = RatelimitSettings(
|
||||
config.get("rc_joins", {}).get("local", {}),
|
||||
defaults={"per_second": 0.1, "burst_count": 10},
|
||||
)
|
||||
self.rc_joins_remote = RateLimitConfig(
|
||||
self.rc_joins_remote = RatelimitSettings(
|
||||
config.get("rc_joins", {}).get("remote", {}),
|
||||
defaults={"per_second": 0.01, "burst_count": 10},
|
||||
)
|
||||
|
||||
# Track the rate of joins to a given room. If there are too many, temporarily
|
||||
# prevent local joins and remote joins via this server.
|
||||
self.rc_joins_per_room = RateLimitConfig(
|
||||
self.rc_joins_per_room = RatelimitSettings(
|
||||
config.get("rc_joins_per_room", {}),
|
||||
defaults={"per_second": 1, "burst_count": 10},
|
||||
)
|
||||
@@ -124,31 +124,31 @@ class RatelimitConfig(Config):
|
||||
# * For requests received over federation this is keyed by the origin.
|
||||
#
|
||||
# Note that this isn't exposed in the configuration as it is obscure.
|
||||
self.rc_key_requests = RateLimitConfig(
|
||||
self.rc_key_requests = RatelimitSettings(
|
||||
config.get("rc_key_requests", {}),
|
||||
defaults={"per_second": 20, "burst_count": 100},
|
||||
)
|
||||
|
||||
self.rc_3pid_validation = RateLimitConfig(
|
||||
self.rc_3pid_validation = RatelimitSettings(
|
||||
config.get("rc_3pid_validation") or {},
|
||||
defaults={"per_second": 0.003, "burst_count": 5},
|
||||
)
|
||||
|
||||
self.rc_invites_per_room = RateLimitConfig(
|
||||
self.rc_invites_per_room = RatelimitSettings(
|
||||
config.get("rc_invites", {}).get("per_room", {}),
|
||||
defaults={"per_second": 0.3, "burst_count": 10},
|
||||
)
|
||||
self.rc_invites_per_user = RateLimitConfig(
|
||||
self.rc_invites_per_user = RatelimitSettings(
|
||||
config.get("rc_invites", {}).get("per_user", {}),
|
||||
defaults={"per_second": 0.003, "burst_count": 5},
|
||||
)
|
||||
|
||||
self.rc_invites_per_issuer = RateLimitConfig(
|
||||
self.rc_invites_per_issuer = RatelimitSettings(
|
||||
config.get("rc_invites", {}).get("per_issuer", {}),
|
||||
defaults={"per_second": 0.3, "burst_count": 10},
|
||||
)
|
||||
|
||||
self.rc_third_party_invite = RateLimitConfig(
|
||||
self.rc_third_party_invite = RatelimitSettings(
|
||||
config.get("rc_third_party_invite", {}),
|
||||
defaults={
|
||||
"per_second": self.rc_message.per_second,
|
||||
|
||||
+51
-11
@@ -30,7 +30,13 @@ from synapse.api.constants import (
|
||||
JoinRules,
|
||||
Membership,
|
||||
)
|
||||
from synapse.api.errors import AuthError, EventSizeError, SynapseError
|
||||
from synapse.api.errors import (
|
||||
AuthError,
|
||||
Codes,
|
||||
EventSizeError,
|
||||
SynapseError,
|
||||
UnstableSpecAuthError,
|
||||
)
|
||||
from synapse.api.room_versions import (
|
||||
KNOWN_ROOM_VERSIONS,
|
||||
EventFormatVersions,
|
||||
@@ -291,7 +297,11 @@ def check_state_dependent_auth_rules(
|
||||
invite_level = get_named_level(auth_dict, "invite", 0)
|
||||
|
||||
if user_level < invite_level:
|
||||
raise AuthError(403, "You don't have permission to invite users")
|
||||
raise UnstableSpecAuthError(
|
||||
403,
|
||||
"You don't have permission to invite users",
|
||||
errcode=Codes.INSUFFICIENT_POWER,
|
||||
)
|
||||
else:
|
||||
logger.debug("Allowing! %s", event)
|
||||
return
|
||||
@@ -474,7 +484,11 @@ def _is_membership_change_allowed(
|
||||
return
|
||||
|
||||
if not caller_in_room: # caller isn't joined
|
||||
raise AuthError(403, "%s not in room %s." % (event.user_id, event.room_id))
|
||||
raise UnstableSpecAuthError(
|
||||
403,
|
||||
"%s not in room %s." % (event.user_id, event.room_id),
|
||||
errcode=Codes.NOT_JOINED,
|
||||
)
|
||||
|
||||
if Membership.INVITE == membership:
|
||||
# TODO (erikj): We should probably handle this more intelligently
|
||||
@@ -484,10 +498,18 @@ def _is_membership_change_allowed(
|
||||
if target_banned:
|
||||
raise AuthError(403, "%s is banned from the room" % (target_user_id,))
|
||||
elif target_in_room: # the target is already in the room.
|
||||
raise AuthError(403, "%s is already in the room." % target_user_id)
|
||||
raise UnstableSpecAuthError(
|
||||
403,
|
||||
"%s is already in the room." % target_user_id,
|
||||
errcode=Codes.ALREADY_JOINED,
|
||||
)
|
||||
else:
|
||||
if user_level < invite_level:
|
||||
raise AuthError(403, "You don't have permission to invite users")
|
||||
raise UnstableSpecAuthError(
|
||||
403,
|
||||
"You don't have permission to invite users",
|
||||
errcode=Codes.INSUFFICIENT_POWER,
|
||||
)
|
||||
elif Membership.JOIN == membership:
|
||||
# Joins are valid iff caller == target and:
|
||||
# * They are not banned.
|
||||
@@ -549,15 +571,27 @@ def _is_membership_change_allowed(
|
||||
elif Membership.LEAVE == membership:
|
||||
# TODO (erikj): Implement kicks.
|
||||
if target_banned and user_level < ban_level:
|
||||
raise AuthError(403, "You cannot unban user %s." % (target_user_id,))
|
||||
raise UnstableSpecAuthError(
|
||||
403,
|
||||
"You cannot unban user %s." % (target_user_id,),
|
||||
errcode=Codes.INSUFFICIENT_POWER,
|
||||
)
|
||||
elif target_user_id != event.user_id:
|
||||
kick_level = get_named_level(auth_events, "kick", 50)
|
||||
|
||||
if user_level < kick_level or user_level <= target_level:
|
||||
raise AuthError(403, "You cannot kick user %s." % target_user_id)
|
||||
raise UnstableSpecAuthError(
|
||||
403,
|
||||
"You cannot kick user %s." % target_user_id,
|
||||
errcode=Codes.INSUFFICIENT_POWER,
|
||||
)
|
||||
elif Membership.BAN == membership:
|
||||
if user_level < ban_level or user_level <= target_level:
|
||||
raise AuthError(403, "You don't have permission to ban")
|
||||
raise UnstableSpecAuthError(
|
||||
403,
|
||||
"You don't have permission to ban",
|
||||
errcode=Codes.INSUFFICIENT_POWER,
|
||||
)
|
||||
elif room_version.msc2403_knocking and Membership.KNOCK == membership:
|
||||
if join_rule != JoinRules.KNOCK and (
|
||||
not room_version.msc3787_knock_restricted_join_rule
|
||||
@@ -567,7 +601,11 @@ def _is_membership_change_allowed(
|
||||
elif target_user_id != event.user_id:
|
||||
raise AuthError(403, "You cannot knock for other users")
|
||||
elif target_in_room:
|
||||
raise AuthError(403, "You cannot knock on a room you are already in")
|
||||
raise UnstableSpecAuthError(
|
||||
403,
|
||||
"You cannot knock on a room you are already in",
|
||||
errcode=Codes.ALREADY_JOINED,
|
||||
)
|
||||
elif caller_invited:
|
||||
raise AuthError(403, "You are already invited to this room")
|
||||
elif target_banned:
|
||||
@@ -638,10 +676,11 @@ def _can_send_event(event: "EventBase", auth_events: StateMap["EventBase"]) -> b
|
||||
user_level = get_user_power_level(event.user_id, auth_events)
|
||||
|
||||
if user_level < send_level:
|
||||
raise AuthError(
|
||||
raise UnstableSpecAuthError(
|
||||
403,
|
||||
"You don't have permission to post that to the room. "
|
||||
+ "user_level (%d) < send_level (%d)" % (user_level, send_level),
|
||||
errcode=Codes.INSUFFICIENT_POWER,
|
||||
)
|
||||
|
||||
# Check state_key
|
||||
@@ -716,9 +755,10 @@ def check_historical(
|
||||
historical_level = get_named_level(auth_events, "historical", 100)
|
||||
|
||||
if user_level < historical_level:
|
||||
raise AuthError(
|
||||
raise UnstableSpecAuthError(
|
||||
403,
|
||||
'You don\'t have permission to send send historical related events ("insertion", "batch", and "marker")',
|
||||
errcode=Codes.INSUFFICIENT_POWER,
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -11,11 +11,10 @@
|
||||
# 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, Tuple, Union
|
||||
from typing import TYPE_CHECKING, List, Optional, Tuple
|
||||
|
||||
import attr
|
||||
from frozendict import frozendict
|
||||
from typing_extensions import Literal
|
||||
|
||||
from synapse.appservice import ApplicationService
|
||||
from synapse.events import EventBase
|
||||
@@ -33,7 +32,7 @@ class EventContext:
|
||||
Holds information relevant to persisting an event
|
||||
|
||||
Attributes:
|
||||
rejected: A rejection reason if the event was rejected, else False
|
||||
rejected: A rejection reason if the event was rejected, else None
|
||||
|
||||
_state_group: The ID of the state group for this event. Note that state events
|
||||
are persisted with a state group which includes the new event, so this is
|
||||
@@ -85,7 +84,7 @@ class EventContext:
|
||||
"""
|
||||
|
||||
_storage: "StorageControllers"
|
||||
rejected: Union[Literal[False], str] = False
|
||||
rejected: Optional[str] = None
|
||||
_state_group: Optional[int] = None
|
||||
state_group_before_event: Optional[int] = None
|
||||
_state_delta_due_to_event: Optional[StateMap[str]] = None
|
||||
|
||||
@@ -61,6 +61,7 @@ from synapse.federation.federation_base import (
|
||||
)
|
||||
from synapse.federation.transport.client import SendJoinResponse
|
||||
from synapse.http.types import QueryParams
|
||||
from synapse.logging.opentracing import trace
|
||||
from synapse.types import JsonDict, UserID, get_domain_from_id
|
||||
from synapse.util.async_helpers import concurrently_execute
|
||||
from synapse.util.caches.expiringcache import ExpiringCache
|
||||
@@ -233,6 +234,7 @@ class FederationClient(FederationBase):
|
||||
destination, content, timeout
|
||||
)
|
||||
|
||||
@trace
|
||||
async def backfill(
|
||||
self, dest: str, room_id: str, limit: int, extremities: Collection[str]
|
||||
) -> Optional[List[EventBase]]:
|
||||
@@ -403,9 +405,9 @@ class FederationClient(FederationBase):
|
||||
# Prime the cache
|
||||
self._get_pdu_cache[event.event_id] = event
|
||||
|
||||
# FIXME: We should add a `break` here to avoid calling every
|
||||
# destination after we already found a PDU (will follow-up
|
||||
# in a separate PR)
|
||||
# Now that we have an event, we can break out of this
|
||||
# loop and stop asking other destinations.
|
||||
break
|
||||
|
||||
except SynapseError as e:
|
||||
logger.info(
|
||||
@@ -725,6 +727,12 @@ class FederationClient(FederationBase):
|
||||
if failover_errcodes is None:
|
||||
failover_errcodes = ()
|
||||
|
||||
if not destinations:
|
||||
# Give a bit of a clearer message if no servers were specified at all.
|
||||
raise SynapseError(
|
||||
502, f"Failed to {description} via any server: No servers specified."
|
||||
)
|
||||
|
||||
for destination in destinations:
|
||||
if destination == self.server_name:
|
||||
continue
|
||||
@@ -774,7 +782,7 @@ class FederationClient(FederationBase):
|
||||
"Failed to %s via %s", description, destination, exc_info=True
|
||||
)
|
||||
|
||||
raise SynapseError(502, "Failed to %s via any server" % (description,))
|
||||
raise SynapseError(502, f"Failed to {description} via any server")
|
||||
|
||||
async def make_membership_event(
|
||||
self,
|
||||
|
||||
@@ -469,7 +469,7 @@ class FederationServer(FederationBase):
|
||||
)
|
||||
for pdu in pdus_by_room[room_id]:
|
||||
event_id = pdu.event_id
|
||||
pdu_results[event_id] = e.error_dict()
|
||||
pdu_results[event_id] = e.error_dict(self.hs.config)
|
||||
return
|
||||
|
||||
for pdu in pdus_by_room[room_id]:
|
||||
@@ -843,8 +843,25 @@ class FederationServer(FederationBase):
|
||||
Codes.BAD_JSON,
|
||||
)
|
||||
|
||||
# Note that get_room_version throws if the room does not exist here.
|
||||
room_version = await self.store.get_room_version(room_id)
|
||||
|
||||
if await self.store.is_partial_state_room(room_id):
|
||||
# If our server is still only partially joined, we can't give a complete
|
||||
# response to /send_join, /send_knock or /send_leave.
|
||||
# This is because we will not be able to provide the server list (for partial
|
||||
# joins) or the full state (for full joins).
|
||||
# Return a 404 as we would if we weren't in the room at all.
|
||||
logger.info(
|
||||
f"Rejecting /send_{membership_type} to %s because it's a partial state room",
|
||||
room_id,
|
||||
)
|
||||
raise SynapseError(
|
||||
404,
|
||||
f"Unable to handle /send_{membership_type} right now; this server is not fully joined.",
|
||||
errcode=Codes.NOT_FOUND,
|
||||
)
|
||||
|
||||
if membership_type == Membership.KNOCK and not room_version.msc2403_knocking:
|
||||
raise SynapseError(
|
||||
403,
|
||||
|
||||
@@ -565,7 +565,7 @@ class AuthHandler:
|
||||
except LoginError as e:
|
||||
# this step failed. Merge the error dict into the response
|
||||
# so that the client can have another go.
|
||||
errordict = e.error_dict()
|
||||
errordict = e.error_dict(self.hs.config)
|
||||
|
||||
creds = await self.store.get_completed_ui_auth_stages(session.session_id)
|
||||
for f in flows:
|
||||
|
||||
@@ -59,6 +59,7 @@ from synapse.events.validator import EventValidator
|
||||
from synapse.federation.federation_client import InvalidResponseError
|
||||
from synapse.http.servlet import assert_params_in_dict
|
||||
from synapse.logging.context import nested_logging_context
|
||||
from synapse.logging.opentracing import trace
|
||||
from synapse.metrics.background_process_metrics import run_as_background_process
|
||||
from synapse.module_api import NOT_SPAM
|
||||
from synapse.replication.http.federation import (
|
||||
@@ -180,6 +181,7 @@ class FederationHandler:
|
||||
"resume_sync_partial_state_room", self._resume_sync_partial_state_room
|
||||
)
|
||||
|
||||
@trace
|
||||
async def maybe_backfill(
|
||||
self, room_id: str, current_depth: int, limit: int
|
||||
) -> bool:
|
||||
@@ -546,9 +548,9 @@ class FederationHandler:
|
||||
)
|
||||
|
||||
if ret.partial_state:
|
||||
# TODO(faster_joins): roll this back if we don't manage to start the
|
||||
# background resync (eg process_remote_join fails)
|
||||
# https://github.com/matrix-org/synapse/issues/12998
|
||||
# Mark the room as having partial state.
|
||||
# The background process is responsible for unmarking this flag,
|
||||
# even if the join fails.
|
||||
await self.store.store_partial_state_room(room_id, ret.servers_in_room)
|
||||
|
||||
try:
|
||||
@@ -574,17 +576,21 @@ class FederationHandler:
|
||||
room_id,
|
||||
)
|
||||
raise LimitExceededError(msg=e.msg, errcode=e.errcode, retry_after_ms=0)
|
||||
|
||||
if ret.partial_state:
|
||||
# Kick off the process of asynchronously fetching the state for this
|
||||
# room.
|
||||
run_as_background_process(
|
||||
desc="sync_partial_state_room",
|
||||
func=self._sync_partial_state_room,
|
||||
initial_destination=origin,
|
||||
other_destinations=ret.servers_in_room,
|
||||
room_id=room_id,
|
||||
)
|
||||
finally:
|
||||
# Always kick off the background process that asynchronously fetches
|
||||
# state for the room.
|
||||
# If the join failed, the background process is responsible for
|
||||
# cleaning up — including unmarking the room as a partial state room.
|
||||
if ret.partial_state:
|
||||
# Kick off the process of asynchronously fetching the state for this
|
||||
# room.
|
||||
run_as_background_process(
|
||||
desc="sync_partial_state_room",
|
||||
func=self._sync_partial_state_room,
|
||||
initial_destination=origin,
|
||||
other_destinations=ret.servers_in_room,
|
||||
room_id=room_id,
|
||||
)
|
||||
|
||||
# We wait here until this instance has seen the events come down
|
||||
# replication (if we're using replication) as the below uses caches.
|
||||
@@ -748,6 +754,23 @@ class FederationHandler:
|
||||
# (and return a 404 otherwise)
|
||||
room_version = await self.store.get_room_version(room_id)
|
||||
|
||||
if await self.store.is_partial_state_room(room_id):
|
||||
# If our server is still only partially joined, we can't give a complete
|
||||
# response to /make_join, so return a 404 as we would if we weren't in the
|
||||
# room at all.
|
||||
# The main reason we can't respond properly is that we need to know about
|
||||
# the auth events for the join event that we would return.
|
||||
# We also should not bother entertaining the /make_join since we cannot
|
||||
# handle the /send_join.
|
||||
logger.info(
|
||||
"Rejecting /make_join to %s because it's a partial state room", room_id
|
||||
)
|
||||
raise SynapseError(
|
||||
404,
|
||||
"Unable to handle /make_join right now; this server is not fully joined.",
|
||||
errcode=Codes.NOT_FOUND,
|
||||
)
|
||||
|
||||
# now check that we are *still* in the room
|
||||
is_in_room = await self._event_auth_handler.check_host_in_room(
|
||||
room_id, self.server_name
|
||||
@@ -1539,15 +1562,16 @@ class FederationHandler:
|
||||
|
||||
# Make an infinite iterator of destinations to try. Once we find a working
|
||||
# destination, we'll stick with it until it flakes.
|
||||
destinations: Collection[str]
|
||||
if initial_destination is not None:
|
||||
# Move `initial_destination` to the front of the list.
|
||||
destinations = list(other_destinations)
|
||||
if initial_destination in destinations:
|
||||
destinations.remove(initial_destination)
|
||||
destinations = [initial_destination] + destinations
|
||||
destination_iter = itertools.cycle(destinations)
|
||||
else:
|
||||
destination_iter = itertools.cycle(other_destinations)
|
||||
destinations = other_destinations
|
||||
destination_iter = itertools.cycle(destinations)
|
||||
|
||||
# `destination` is the current remote homeserver we're pulling from.
|
||||
destination = next(destination_iter)
|
||||
|
||||
@@ -59,6 +59,7 @@ from synapse.events import EventBase
|
||||
from synapse.events.snapshot import EventContext
|
||||
from synapse.federation.federation_client import InvalidResponseError
|
||||
from synapse.logging.context import nested_logging_context
|
||||
from synapse.logging.opentracing import trace
|
||||
from synapse.metrics.background_process_metrics import run_as_background_process
|
||||
from synapse.replication.http.devices import ReplicationUserDevicesResyncRestServlet
|
||||
from synapse.replication.http.federation import (
|
||||
@@ -278,7 +279,8 @@ class FederationEventHandler:
|
||||
)
|
||||
|
||||
try:
|
||||
await self._process_received_pdu(origin, pdu, state_ids=None)
|
||||
context = await self._state_handler.compute_event_context(pdu)
|
||||
await self._process_received_pdu(origin, pdu, context)
|
||||
except PartialStateConflictError:
|
||||
# The room was un-partial stated while we were processing the PDU.
|
||||
# Try once more, with full state this time.
|
||||
@@ -286,7 +288,8 @@ class FederationEventHandler:
|
||||
"Room %s was un-partial stated while processing the PDU, trying again.",
|
||||
room_id,
|
||||
)
|
||||
await self._process_received_pdu(origin, pdu, state_ids=None)
|
||||
context = await self._state_handler.compute_event_context(pdu)
|
||||
await self._process_received_pdu(origin, pdu, context)
|
||||
|
||||
async def on_send_membership_event(
|
||||
self, origin: str, event: EventBase
|
||||
@@ -316,6 +319,7 @@ class FederationEventHandler:
|
||||
The event and context of the event after inserting it into the room graph.
|
||||
|
||||
Raises:
|
||||
RuntimeError if any prev_events are missing
|
||||
SynapseError if the event is not accepted into the room
|
||||
PartialStateConflictError if the room was un-partial stated in between
|
||||
computing the state at the event and persisting it. The caller should
|
||||
@@ -376,7 +380,7 @@ class FederationEventHandler:
|
||||
# need to.
|
||||
await self._event_creation_handler.cache_joined_hosts_for_event(event, context)
|
||||
|
||||
await self._check_for_soft_fail(event, None, origin=origin)
|
||||
await self._check_for_soft_fail(event, context=context, origin=origin)
|
||||
await self._run_push_actions_and_persist_event(event, context)
|
||||
return event, context
|
||||
|
||||
@@ -534,32 +538,36 @@ class FederationEventHandler:
|
||||
#
|
||||
# This is the same operation as we do when we receive a regular event
|
||||
# over federation.
|
||||
state_ids = await self._resolve_state_at_missing_prevs(destination, event)
|
||||
|
||||
# build a new state group for it if need be
|
||||
context = await self._state_handler.compute_event_context(
|
||||
event,
|
||||
state_ids_before_event=state_ids,
|
||||
context = await self._compute_event_context_with_maybe_missing_prevs(
|
||||
destination, event
|
||||
)
|
||||
if context.partial_state:
|
||||
# this can happen if some or all of the event's prev_events still have
|
||||
# partial state - ie, an event has an earlier stream_ordering than one
|
||||
# or more of its prev_events, so we de-partial-state it before its
|
||||
# prev_events.
|
||||
# partial state. We were careful to only pick events from the db without
|
||||
# partial-state prev events, so that implies that a prev event has
|
||||
# been persisted (with partial state) since we did the query.
|
||||
#
|
||||
# TODO(faster_joins): we probably need to be more intelligent, and
|
||||
# exclude partial-state prev_events from consideration
|
||||
# https://github.com/matrix-org/synapse/issues/13001
|
||||
# So, let's just ignore `event` for now; when we re-run the db query
|
||||
# we should instead get its partial-state prev event, which we will
|
||||
# de-partial-state, and then come back to event.
|
||||
logger.warning(
|
||||
"%s still has partial state: can't de-partial-state it yet",
|
||||
"%s still has prev_events with partial state: can't de-partial-state it yet",
|
||||
event.event_id,
|
||||
)
|
||||
return
|
||||
|
||||
# since the state at this event has changed, we should now re-evaluate
|
||||
# whether it should have been rejected. We must already have all of the
|
||||
# auth events (from last time we went round this path), so there is no
|
||||
# need to pass the origin.
|
||||
await self._check_event_auth(None, event, context)
|
||||
|
||||
await self._store.update_state_for_partial_state_event(event, context)
|
||||
self._state_storage_controller.notify_event_un_partial_stated(
|
||||
event.event_id
|
||||
)
|
||||
|
||||
@trace
|
||||
async def backfill(
|
||||
self, dest: str, room_id: str, limit: int, extremities: Collection[str]
|
||||
) -> None:
|
||||
@@ -604,6 +612,7 @@ class FederationEventHandler:
|
||||
backfilled=True,
|
||||
)
|
||||
|
||||
@trace
|
||||
async def _get_missing_events_for_pdu(
|
||||
self, origin: str, pdu: EventBase, prevs: Set[str], min_depth: int
|
||||
) -> None:
|
||||
@@ -704,6 +713,7 @@ class FederationEventHandler:
|
||||
logger.info("Got %d prev_events", len(missing_events))
|
||||
await self._process_pulled_events(origin, missing_events, backfilled=False)
|
||||
|
||||
@trace
|
||||
async def _process_pulled_events(
|
||||
self, origin: str, events: Iterable[EventBase], backfilled: bool
|
||||
) -> None:
|
||||
@@ -742,6 +752,7 @@ class FederationEventHandler:
|
||||
with nested_logging_context(ev.event_id):
|
||||
await self._process_pulled_event(origin, ev, backfilled=backfilled)
|
||||
|
||||
@trace
|
||||
async def _process_pulled_event(
|
||||
self, origin: str, event: EventBase, backfilled: bool
|
||||
) -> None:
|
||||
@@ -806,29 +817,55 @@ class FederationEventHandler:
|
||||
return
|
||||
|
||||
try:
|
||||
state_ids = await self._resolve_state_at_missing_prevs(origin, event)
|
||||
# TODO(faster_joins): make sure that _resolve_state_at_missing_prevs does
|
||||
# not return partial state
|
||||
# https://github.com/matrix-org/synapse/issues/13002
|
||||
try:
|
||||
context = await self._compute_event_context_with_maybe_missing_prevs(
|
||||
origin, event
|
||||
)
|
||||
await self._process_received_pdu(
|
||||
origin,
|
||||
event,
|
||||
context,
|
||||
backfilled=backfilled,
|
||||
)
|
||||
except PartialStateConflictError:
|
||||
# The room was un-partial stated while we were processing the event.
|
||||
# Try once more, with full state this time.
|
||||
context = await self._compute_event_context_with_maybe_missing_prevs(
|
||||
origin, event
|
||||
)
|
||||
|
||||
await self._process_received_pdu(
|
||||
origin, event, state_ids=state_ids, backfilled=backfilled
|
||||
)
|
||||
# We ought to have full state now, barring some unlikely race where we left and
|
||||
# rejoned the room in the background.
|
||||
if context.partial_state:
|
||||
raise AssertionError(
|
||||
f"Event {event.event_id} still has a partial resolved state "
|
||||
f"after room {event.room_id} was un-partial stated"
|
||||
)
|
||||
|
||||
await self._process_received_pdu(
|
||||
origin,
|
||||
event,
|
||||
context,
|
||||
backfilled=backfilled,
|
||||
)
|
||||
except FederationError as e:
|
||||
if e.code == 403:
|
||||
logger.warning("Pulled event %s failed history check.", event_id)
|
||||
else:
|
||||
raise
|
||||
|
||||
async def _resolve_state_at_missing_prevs(
|
||||
async def _compute_event_context_with_maybe_missing_prevs(
|
||||
self, dest: str, event: EventBase
|
||||
) -> Optional[StateMap[str]]:
|
||||
"""Calculate the state at an event with missing prev_events.
|
||||
) -> EventContext:
|
||||
"""Build an EventContext structure for a non-outlier event whose prev_events may
|
||||
be missing.
|
||||
|
||||
This is used when we have pulled a batch of events from a remote server, and
|
||||
still don't have all the prev_events.
|
||||
This is used when we have pulled a batch of events from a remote server, and may
|
||||
not have all the prev_events.
|
||||
|
||||
If we already have all the prev_events for `event`, this method does nothing.
|
||||
To build an EventContext, we need to calculate the state before the event. If we
|
||||
already have all the prev_events for `event`, we can simply use the state after
|
||||
the prev_events to calculate the state before `event`.
|
||||
|
||||
Otherwise, the missing prevs become new backwards extremities, and we fall back
|
||||
to asking the remote server for the state after each missing `prev_event`,
|
||||
@@ -849,8 +886,7 @@ class FederationEventHandler:
|
||||
event: an event to check for missing prevs.
|
||||
|
||||
Returns:
|
||||
if we already had all the prev events, `None`. Otherwise, returns
|
||||
the event ids of the state at `event`.
|
||||
The event context.
|
||||
|
||||
Raises:
|
||||
FederationError if we fail to get the state from the remote server after any
|
||||
@@ -864,7 +900,7 @@ class FederationEventHandler:
|
||||
missing_prevs = prevs - seen
|
||||
|
||||
if not missing_prevs:
|
||||
return None
|
||||
return await self._state_handler.compute_event_context(event)
|
||||
|
||||
logger.info(
|
||||
"Event %s is missing prev_events %s: calculating state for a "
|
||||
@@ -876,9 +912,15 @@ class FederationEventHandler:
|
||||
# resolve them to find the correct state at the current event.
|
||||
|
||||
try:
|
||||
# Determine whether we may be about to retrieve partial state
|
||||
# Events may be un-partial stated right after we compute the partial state
|
||||
# flag, but that's okay, as long as the flag errs on the conservative side.
|
||||
partial_state_flags = await self._store.get_partial_state_events(seen)
|
||||
partial_state = any(partial_state_flags.values())
|
||||
|
||||
# Get the state of the events we know about
|
||||
ours = await self._state_storage_controller.get_state_groups_ids(
|
||||
room_id, seen
|
||||
room_id, seen, await_full_state=False
|
||||
)
|
||||
|
||||
# state_maps is a list of mappings from (type, state_key) to event_id
|
||||
@@ -924,7 +966,9 @@ class FederationEventHandler:
|
||||
"We can't get valid state history.",
|
||||
affected=event_id,
|
||||
)
|
||||
return state_map
|
||||
return await self._state_handler.compute_event_context(
|
||||
event, state_ids_before_event=state_map, partial_state=partial_state
|
||||
)
|
||||
|
||||
async def _get_state_ids_after_missing_prev_event(
|
||||
self,
|
||||
@@ -1093,7 +1137,7 @@ class FederationEventHandler:
|
||||
self,
|
||||
origin: str,
|
||||
event: EventBase,
|
||||
state_ids: Optional[StateMap[str]],
|
||||
context: EventContext,
|
||||
backfilled: bool = False,
|
||||
) -> None:
|
||||
"""Called when we have a new non-outlier event.
|
||||
@@ -1115,24 +1159,18 @@ class FederationEventHandler:
|
||||
|
||||
event: event to be persisted
|
||||
|
||||
state_ids: Normally None, but if we are handling a gap in the graph
|
||||
(ie, we are missing one or more prev_events), the resolved state at the
|
||||
event. Must not be partial state.
|
||||
context: The `EventContext` to persist the event with.
|
||||
|
||||
backfilled: True if this is part of a historical batch of events (inhibits
|
||||
notification to clients, and validation of device keys.)
|
||||
|
||||
PartialStateConflictError: if the room was un-partial stated in between
|
||||
computing the state at the event and persisting it. The caller should retry
|
||||
exactly once in this case. Will never be raised if `state_ids` is provided.
|
||||
computing the state at the event and persisting it. The caller should
|
||||
recompute `context` and retry exactly once when this happens.
|
||||
"""
|
||||
logger.debug("Processing event: %s", event)
|
||||
assert not event.internal_metadata.outlier
|
||||
|
||||
context = await self._state_handler.compute_event_context(
|
||||
event,
|
||||
state_ids_before_event=state_ids,
|
||||
)
|
||||
try:
|
||||
await self._check_event_auth(origin, event, context)
|
||||
except AuthError as e:
|
||||
@@ -1144,7 +1182,7 @@ class FederationEventHandler:
|
||||
# For new (non-backfilled and non-outlier) events we check if the event
|
||||
# passes auth based on the current state. If it doesn't then we
|
||||
# "soft-fail" the event.
|
||||
await self._check_for_soft_fail(event, state_ids, origin=origin)
|
||||
await self._check_for_soft_fail(event, context=context, origin=origin)
|
||||
|
||||
await self._run_push_actions_and_persist_event(event, context, backfilled)
|
||||
|
||||
@@ -1556,13 +1594,15 @@ class FederationEventHandler:
|
||||
)
|
||||
|
||||
async def _check_event_auth(
|
||||
self, origin: str, event: EventBase, context: EventContext
|
||||
self, origin: Optional[str], event: EventBase, context: EventContext
|
||||
) -> None:
|
||||
"""
|
||||
Checks whether an event should be rejected (for failing auth checks).
|
||||
|
||||
Args:
|
||||
origin: The host the event originates from.
|
||||
origin: The host the event originates from. This is used to fetch
|
||||
any missing auth events. It can be set to None, but only if we are
|
||||
sure that we already have all the auth events.
|
||||
event: The event itself.
|
||||
context:
|
||||
The event context.
|
||||
@@ -1705,7 +1745,7 @@ class FederationEventHandler:
|
||||
async def _check_for_soft_fail(
|
||||
self,
|
||||
event: EventBase,
|
||||
state_ids: Optional[StateMap[str]],
|
||||
context: EventContext,
|
||||
origin: str,
|
||||
) -> None:
|
||||
"""Checks if we should soft fail the event; if so, marks the event as
|
||||
@@ -1716,7 +1756,7 @@ class FederationEventHandler:
|
||||
|
||||
Args:
|
||||
event
|
||||
state_ids: The state at the event if we don't have all the event's prev events
|
||||
context: The `EventContext` which we are about to persist the event with.
|
||||
origin: The host the event originates from.
|
||||
"""
|
||||
if await self._store.is_partial_state_room(event.room_id):
|
||||
@@ -1742,11 +1782,15 @@ class FederationEventHandler:
|
||||
auth_types = auth_types_for_event(room_version_obj, event)
|
||||
|
||||
# Calculate the "current state".
|
||||
if state_ids is not None:
|
||||
# If we're explicitly given the state then we won't have all the
|
||||
# prev events, and so we have a gap in the graph. In this case
|
||||
# we want to be a little careful as we might have been down for
|
||||
# a while and have an incorrect view of the current state,
|
||||
seen_event_ids = await self._store.have_events_in_timeline(prev_event_ids)
|
||||
has_missing_prevs = bool(prev_event_ids - seen_event_ids)
|
||||
if has_missing_prevs:
|
||||
# We don't have all the prev_events of this event, which means we have a
|
||||
# gap in the graph, and the new event is going to become a new backwards
|
||||
# extremity.
|
||||
#
|
||||
# In this case we want to be a little careful as we might have been
|
||||
# down for a while and have an incorrect view of the current state,
|
||||
# however we still want to do checks as gaps are easy to
|
||||
# maliciously manufacture.
|
||||
#
|
||||
@@ -1759,6 +1803,7 @@ class FederationEventHandler:
|
||||
event.room_id, extrem_ids
|
||||
)
|
||||
state_sets: List[StateMap[str]] = list(state_sets_d.values())
|
||||
state_ids = await context.get_prev_state_ids()
|
||||
state_sets.append(state_ids)
|
||||
current_state_ids = (
|
||||
await self._state_resolution_handler.resolve_events_with_store(
|
||||
@@ -1808,7 +1853,7 @@ class FederationEventHandler:
|
||||
event.internal_metadata.soft_failed = True
|
||||
|
||||
async def _load_or_fetch_auth_events_for_event(
|
||||
self, destination: str, event: EventBase
|
||||
self, destination: Optional[str], event: EventBase
|
||||
) -> Collection[EventBase]:
|
||||
"""Fetch this event's auth_events, from database or remote
|
||||
|
||||
@@ -1824,12 +1869,19 @@ class FederationEventHandler:
|
||||
Args:
|
||||
destination: where to send the /event_auth request. Typically the server
|
||||
that sent us `event` in the first place.
|
||||
|
||||
If this is None, no attempt is made to load any missing auth events:
|
||||
rather, an AssertionError is raised if there are any missing events.
|
||||
|
||||
event: the event whose auth_events we want
|
||||
|
||||
Returns:
|
||||
all of the events listed in `event.auth_events_ids`, after deduplication
|
||||
|
||||
Raises:
|
||||
AssertionError if some auth events were missing and no `destination` was
|
||||
supplied.
|
||||
|
||||
AuthError if we were unable to fetch the auth_events for any reason.
|
||||
"""
|
||||
event_auth_event_ids = set(event.auth_event_ids())
|
||||
@@ -1841,6 +1893,13 @@ class FederationEventHandler:
|
||||
)
|
||||
if not missing_auth_event_ids:
|
||||
return event_auth_events.values()
|
||||
if destination is None:
|
||||
# this shouldn't happen: destination must be set unless we know we have already
|
||||
# persisted the auth events.
|
||||
raise AssertionError(
|
||||
"_load_or_fetch_auth_events_for_event() called with no destination for "
|
||||
"an event with missing auth_events"
|
||||
)
|
||||
|
||||
logger.info(
|
||||
"Event %s refers to unknown auth events %s: fetching auth chain",
|
||||
|
||||
@@ -143,8 +143,8 @@ class InitialSyncHandler:
|
||||
joined_rooms,
|
||||
to_key=int(now_token.receipt_key),
|
||||
)
|
||||
if self.hs.config.experimental.msc2285_enabled:
|
||||
receipt = ReceiptEventSource.filter_out_private_receipts(receipt, user_id)
|
||||
|
||||
receipt = ReceiptEventSource.filter_out_private_receipts(receipt, user_id)
|
||||
|
||||
tags_by_room = await self.store.get_tags_for_user(user_id)
|
||||
|
||||
@@ -456,11 +456,8 @@ class InitialSyncHandler:
|
||||
)
|
||||
if not receipts:
|
||||
return []
|
||||
if self.hs.config.experimental.msc2285_enabled:
|
||||
receipts = ReceiptEventSource.filter_out_private_receipts(
|
||||
receipts, user_id
|
||||
)
|
||||
return receipts
|
||||
|
||||
return ReceiptEventSource.filter_out_private_receipts(receipts, user_id)
|
||||
|
||||
presence, receipts, (messages, token) = await make_deferred_yieldable(
|
||||
gather_results(
|
||||
|
||||
+28
-10
@@ -41,6 +41,7 @@ from synapse.api.errors import (
|
||||
NotFoundError,
|
||||
ShadowBanError,
|
||||
SynapseError,
|
||||
UnstableSpecAuthError,
|
||||
UnsupportedRoomVersionError,
|
||||
)
|
||||
from synapse.api.room_versions import KNOWN_ROOM_VERSIONS
|
||||
@@ -51,6 +52,7 @@ from synapse.events.builder import EventBuilder
|
||||
from synapse.events.snapshot import EventContext
|
||||
from synapse.events.validator import EventValidator
|
||||
from synapse.handlers.directory import DirectoryHandler
|
||||
from synapse.logging import opentracing
|
||||
from synapse.logging.context import make_deferred_yieldable, run_in_background
|
||||
from synapse.metrics.background_process_metrics import run_as_background_process
|
||||
from synapse.replication.http.send_event import ReplicationSendEventRestServlet
|
||||
@@ -149,7 +151,11 @@ class MessageHandler:
|
||||
"Attempted to retrieve data from a room for a user that has never been in it. "
|
||||
"This should not have happened."
|
||||
)
|
||||
raise SynapseError(403, "User not in room", errcode=Codes.FORBIDDEN)
|
||||
raise UnstableSpecAuthError(
|
||||
403,
|
||||
"User not in room",
|
||||
errcode=Codes.NOT_JOINED,
|
||||
)
|
||||
|
||||
return data
|
||||
|
||||
@@ -319,8 +325,10 @@ class MessageHandler:
|
||||
room_id, user_id, allow_departed_users=True
|
||||
)
|
||||
if membership != Membership.JOIN:
|
||||
raise NotImplementedError(
|
||||
"Getting joined members after leaving is not implemented"
|
||||
raise SynapseError(
|
||||
code=403,
|
||||
errcode=Codes.FORBIDDEN,
|
||||
msg="Getting joined members while not being a current member of the room is forbidden.",
|
||||
)
|
||||
|
||||
users_with_profile = await self.store.get_users_in_room_with_profiles(room_id)
|
||||
@@ -334,7 +342,11 @@ class MessageHandler:
|
||||
break
|
||||
else:
|
||||
# Loop fell through, AS has no interested users in room
|
||||
raise AuthError(403, "Appservice not in room")
|
||||
raise UnstableSpecAuthError(
|
||||
403,
|
||||
"Appservice not in room",
|
||||
errcode=Codes.NOT_JOINED,
|
||||
)
|
||||
|
||||
return {
|
||||
user_id: {
|
||||
@@ -1135,6 +1147,10 @@ class EventCreationHandler:
|
||||
context = await self.state.compute_event_context(
|
||||
event,
|
||||
state_ids_before_event=state_map_for_event,
|
||||
# TODO(faster_joins): check how MSC2716 works and whether we can have
|
||||
# partial state here
|
||||
# https://github.com/matrix-org/synapse/issues/13003
|
||||
partial_state=False,
|
||||
)
|
||||
else:
|
||||
context = await self.state.compute_event_context(event)
|
||||
@@ -1359,9 +1375,10 @@ class EventCreationHandler:
|
||||
# and `state_groups` because they have `prev_events` that aren't persisted yet
|
||||
# (historical messages persisted in reverse-chronological order).
|
||||
if not event.internal_metadata.is_historical():
|
||||
await self._bulk_push_rule_evaluator.action_for_event_by_user(
|
||||
event, context
|
||||
)
|
||||
with opentracing.start_active_span("calculate_push_actions"):
|
||||
await self._bulk_push_rule_evaluator.action_for_event_by_user(
|
||||
event, context
|
||||
)
|
||||
|
||||
try:
|
||||
# If we're a worker we need to hit out to the master.
|
||||
@@ -1448,9 +1465,10 @@ class EventCreationHandler:
|
||||
state = await state_entry.get_state(
|
||||
self._storage_controllers.state, StateFilter.all()
|
||||
)
|
||||
joined_hosts = await self.store.get_joined_hosts(
|
||||
event.room_id, state, state_entry
|
||||
)
|
||||
with opentracing.start_active_span("get_joined_hosts"):
|
||||
joined_hosts = await self.store.get_joined_hosts(
|
||||
event.room_id, state, state_entry
|
||||
)
|
||||
|
||||
# Note that the expiry times must be larger than the expiry time in
|
||||
# _external_cache_joined_hosts_updates.
|
||||
|
||||
@@ -24,6 +24,7 @@ from synapse.api.errors import SynapseError
|
||||
from synapse.api.filtering import Filter
|
||||
from synapse.events.utils import SerializeEventConfig
|
||||
from synapse.handlers.room import ShutdownRoomResponse
|
||||
from synapse.logging.opentracing import trace
|
||||
from synapse.metrics.background_process_metrics import run_as_background_process
|
||||
from synapse.storage.state import StateFilter
|
||||
from synapse.streams.config import PaginationConfig
|
||||
@@ -416,6 +417,7 @@ class PaginationHandler:
|
||||
|
||||
await self._storage_controllers.purge_events.purge_room(room_id)
|
||||
|
||||
@trace
|
||||
async def get_messages(
|
||||
self,
|
||||
requester: Requester,
|
||||
|
||||
@@ -163,7 +163,10 @@ class ReceiptsHandler:
|
||||
if not is_new:
|
||||
return
|
||||
|
||||
if self.federation_sender and receipt_type != ReceiptTypes.READ_PRIVATE:
|
||||
if self.federation_sender and receipt_type not in (
|
||||
ReceiptTypes.READ_PRIVATE,
|
||||
ReceiptTypes.UNSTABLE_READ_PRIVATE,
|
||||
):
|
||||
await self.federation_sender.send_read_receipt(receipt)
|
||||
|
||||
|
||||
@@ -203,24 +206,38 @@ class ReceiptEventSource(EventSource[int, JsonDict]):
|
||||
for event_id, orig_event_content in room.get("content", {}).items():
|
||||
event_content = orig_event_content
|
||||
# If there are private read receipts, additional logic is necessary.
|
||||
if ReceiptTypes.READ_PRIVATE in event_content:
|
||||
if (
|
||||
ReceiptTypes.READ_PRIVATE in event_content
|
||||
or ReceiptTypes.UNSTABLE_READ_PRIVATE in event_content
|
||||
):
|
||||
# Make a copy without private read receipts to avoid leaking
|
||||
# other user's private read receipts..
|
||||
event_content = {
|
||||
receipt_type: receipt_value
|
||||
for receipt_type, receipt_value in event_content.items()
|
||||
if receipt_type != ReceiptTypes.READ_PRIVATE
|
||||
if receipt_type
|
||||
not in (
|
||||
ReceiptTypes.READ_PRIVATE,
|
||||
ReceiptTypes.UNSTABLE_READ_PRIVATE,
|
||||
)
|
||||
}
|
||||
|
||||
# Copy the current user's private read receipt from the
|
||||
# original content, if it exists.
|
||||
user_private_read_receipt = orig_event_content[
|
||||
ReceiptTypes.READ_PRIVATE
|
||||
].get(user_id, None)
|
||||
user_private_read_receipt = orig_event_content.get(
|
||||
ReceiptTypes.READ_PRIVATE, {}
|
||||
).get(user_id, None)
|
||||
if user_private_read_receipt:
|
||||
event_content[ReceiptTypes.READ_PRIVATE] = {
|
||||
user_id: user_private_read_receipt
|
||||
}
|
||||
user_unstable_private_read_receipt = orig_event_content.get(
|
||||
ReceiptTypes.UNSTABLE_READ_PRIVATE, {}
|
||||
).get(user_id, None)
|
||||
if user_unstable_private_read_receipt:
|
||||
event_content[ReceiptTypes.UNSTABLE_READ_PRIVATE] = {
|
||||
user_id: user_unstable_private_read_receipt
|
||||
}
|
||||
|
||||
# Include the event if there is at least one non-private read
|
||||
# receipt or the current user has a private read receipt.
|
||||
@@ -256,10 +273,9 @@ class ReceiptEventSource(EventSource[int, JsonDict]):
|
||||
room_ids, from_key=from_key, to_key=to_key
|
||||
)
|
||||
|
||||
if self.config.experimental.msc2285_enabled:
|
||||
events = ReceiptEventSource.filter_out_private_receipts(
|
||||
events, user.to_string()
|
||||
)
|
||||
events = ReceiptEventSource.filter_out_private_receipts(
|
||||
events, user.to_string()
|
||||
)
|
||||
|
||||
return events, to_key
|
||||
|
||||
|
||||
@@ -19,6 +19,7 @@ import attr
|
||||
from synapse.api.constants import RelationTypes
|
||||
from synapse.api.errors import SynapseError
|
||||
from synapse.events import EventBase, relation_from_event
|
||||
from synapse.logging.opentracing import trace
|
||||
from synapse.storage.databases.main.relations import _RelatedEvent
|
||||
from synapse.types import JsonDict, Requester, StreamToken, UserID
|
||||
from synapse.visibility import filter_events_for_client
|
||||
@@ -73,7 +74,6 @@ class RelationsHandler:
|
||||
room_id: str,
|
||||
relation_type: Optional[str] = None,
|
||||
event_type: Optional[str] = None,
|
||||
aggregation_key: Optional[str] = None,
|
||||
limit: int = 5,
|
||||
direction: str = "b",
|
||||
from_token: Optional[StreamToken] = None,
|
||||
@@ -89,7 +89,6 @@ class RelationsHandler:
|
||||
room_id: The room the event belongs to.
|
||||
relation_type: Only fetch events with this relation type, if given.
|
||||
event_type: Only fetch events with this event type, if given.
|
||||
aggregation_key: Only fetch events with this aggregation key, if given.
|
||||
limit: Only fetch the most recent `limit` events.
|
||||
direction: Whether to fetch the most recent first (`"b"`) or the
|
||||
oldest first (`"f"`).
|
||||
@@ -122,7 +121,6 @@ class RelationsHandler:
|
||||
room_id=room_id,
|
||||
relation_type=relation_type,
|
||||
event_type=event_type,
|
||||
aggregation_key=aggregation_key,
|
||||
limit=limit,
|
||||
direction=direction,
|
||||
from_token=from_token,
|
||||
@@ -364,6 +362,7 @@ class RelationsHandler:
|
||||
|
||||
return results
|
||||
|
||||
@trace
|
||||
async def get_bundled_aggregations(
|
||||
self, events: Iterable[EventBase], user_id: str
|
||||
) -> Dict[str, BundledAggregations]:
|
||||
|
||||
+77
-58
@@ -558,7 +558,7 @@ class RoomCreationHandler:
|
||||
new_room_id,
|
||||
# we expect to override all the presets with initial_state, so this is
|
||||
# somewhat arbitrary.
|
||||
preset_config=RoomCreationPreset.PRIVATE_CHAT,
|
||||
room_preset_identifier=RoomCreationPreset.PRIVATE_CHAT,
|
||||
invite_list=[],
|
||||
initial_state=initial_state,
|
||||
creation_content=creation_content,
|
||||
@@ -871,7 +871,7 @@ class RoomCreationHandler:
|
||||
check_membership=False,
|
||||
)
|
||||
|
||||
preset_config = config.get(
|
||||
room_preset_identifier = config.get(
|
||||
"preset",
|
||||
RoomCreationPreset.PRIVATE_CHAT
|
||||
if visibility == "private"
|
||||
@@ -896,7 +896,7 @@ class RoomCreationHandler:
|
||||
) = await self._send_events_for_new_room(
|
||||
requester,
|
||||
room_id,
|
||||
preset_config=preset_config,
|
||||
room_preset_identifier=room_preset_identifier,
|
||||
invite_list=invite_list,
|
||||
initial_state=initial_state,
|
||||
creation_content=creation_content,
|
||||
@@ -1020,9 +1020,9 @@ class RoomCreationHandler:
|
||||
self,
|
||||
creator: Requester,
|
||||
room_id: str,
|
||||
preset_config: str,
|
||||
room_preset_identifier: str,
|
||||
invite_list: List[str],
|
||||
initial_state: MutableStateMap,
|
||||
initial_state: MutableStateMap[JsonDict],
|
||||
creation_content: JsonDict,
|
||||
room_alias: Optional[RoomAlias] = None,
|
||||
power_level_content_override: Optional[JsonDict] = None,
|
||||
@@ -1031,8 +1031,24 @@ class RoomCreationHandler:
|
||||
) -> Tuple[int, str, int]:
|
||||
"""Sends the initial events into a new room.
|
||||
|
||||
`power_level_content_override` doesn't apply when initial state has
|
||||
power level state event content.
|
||||
Args:
|
||||
creator: The creator of the room.
|
||||
room_id: The ID of the room to send events in.
|
||||
invite_list: A list of Matrix user IDs to invite to the room. This is only
|
||||
used by the method to set the power levels of the invitees to 100 if
|
||||
the preset for the room specifies that initial invitees should have ops.
|
||||
initial_state: A map of state key to an event definition or event ID.
|
||||
creation_content: A json dict to use as the value of the "content" field
|
||||
for the room's create event.
|
||||
room_preset_identifier: The identifier of the room preset to use. This
|
||||
determines the events that are sent into the room.
|
||||
room_alias: A room alias to link to the room, if provided.
|
||||
power_level_content_override: A json dictionary that specifies the initial
|
||||
power levels of the room. If `initial_state` contains a 'm.room.power_levels'
|
||||
event then this argument will be ignored.
|
||||
creator_join_profile: The JSON dictionary value of the "content" field for the
|
||||
'm.room.membership' join event that will be sent for the creator of the room.
|
||||
ratelimit: Whether to ratelimit the join event of the room creator.
|
||||
|
||||
Returns:
|
||||
A tuple containing the stream ID, event ID and depth of the last
|
||||
@@ -1054,7 +1070,7 @@ class RoomCreationHandler:
|
||||
|
||||
return e
|
||||
|
||||
async def send(etype: str, content: JsonDict, **kwargs: Any) -> int:
|
||||
async def send(etype: str, content: JsonDict, **kwargs: Any) -> None:
|
||||
nonlocal last_sent_event_id
|
||||
nonlocal depth
|
||||
|
||||
@@ -1064,7 +1080,7 @@ class RoomCreationHandler:
|
||||
# allow the room creation to complete.
|
||||
(
|
||||
sent_event,
|
||||
last_stream_id,
|
||||
last_sent_stream_id,
|
||||
) = await self.event_creation_handler.create_and_send_nonmember_event(
|
||||
creator,
|
||||
event,
|
||||
@@ -1079,22 +1095,26 @@ class RoomCreationHandler:
|
||||
last_sent_event_id = sent_event.event_id
|
||||
depth += 1
|
||||
|
||||
return last_stream_id
|
||||
|
||||
try:
|
||||
config = self._presets_dict[preset_config]
|
||||
room_preset_config = self._presets_dict[room_preset_identifier]
|
||||
except KeyError:
|
||||
raise SynapseError(
|
||||
400, f"'{preset_config}' is not a valid preset", errcode=Codes.BAD_JSON
|
||||
400,
|
||||
f"'{room_preset_identifier}' is not a valid preset",
|
||||
errcode=Codes.BAD_JSON,
|
||||
)
|
||||
|
||||
# Create and send the 'm.room.create' event
|
||||
creation_content.update({"creator": creator_id})
|
||||
await send(etype=EventTypes.Create, content=creation_content)
|
||||
|
||||
logger.debug("Sending %s in new room", EventTypes.Member)
|
||||
# Room create event must exist at this point
|
||||
assert last_sent_event_id is not None
|
||||
member_event_id, _ = await self.room_member_handler.update_membership(
|
||||
|
||||
# Create and send the join event for the room creator
|
||||
logger.debug("Sending %s in new room", EventTypes.Member)
|
||||
(
|
||||
last_sent_event_id,
|
||||
last_sent_stream_id,
|
||||
) = await self.room_member_handler.update_membership(
|
||||
creator,
|
||||
creator.user,
|
||||
room_id,
|
||||
@@ -1105,15 +1125,16 @@ class RoomCreationHandler:
|
||||
prev_event_ids=[last_sent_event_id],
|
||||
depth=depth,
|
||||
)
|
||||
last_sent_event_id = member_event_id
|
||||
|
||||
# Create a map of (event_type, state_key) -> event json dict.
|
||||
# A dict allows us to easily load up and override state event definitions
|
||||
state_to_send: MutableStateMap[JsonDict] = {}
|
||||
|
||||
# We treat the power levels override specially as this needs to be one
|
||||
# of the first events that get sent into a room.
|
||||
pl_content = initial_state.pop((EventTypes.PowerLevels, ""), None)
|
||||
if pl_content is not None:
|
||||
last_sent_stream_id = await send(
|
||||
etype=EventTypes.PowerLevels, content=pl_content
|
||||
)
|
||||
state_to_send[(EventTypes.PowerLevels, "")] = pl_content
|
||||
else:
|
||||
power_level_content: JsonDict = {
|
||||
"users": {creator_id: 100},
|
||||
@@ -1137,18 +1158,22 @@ class RoomCreationHandler:
|
||||
"historical": 100,
|
||||
}
|
||||
|
||||
if config["original_invitees_have_ops"]:
|
||||
if room_preset_config["original_invitees_have_ops"]:
|
||||
for invitee in invite_list:
|
||||
power_level_content["users"][invitee] = 100
|
||||
|
||||
# If the user supplied a preset name e.g. "private_chat",
|
||||
# we apply that preset
|
||||
power_level_content.update(config["power_level_content_override"])
|
||||
power_level_content.update(
|
||||
room_preset_config["power_level_content_override"]
|
||||
)
|
||||
|
||||
# If the server config contains default_power_level_content_override,
|
||||
# If the homeserver config contains default_power_level_content_override,
|
||||
# and that contains information for this room preset, apply it.
|
||||
if self._default_power_level_content_override:
|
||||
override = self._default_power_level_content_override.get(preset_config)
|
||||
override = self._default_power_level_content_override.get(
|
||||
room_preset_identifier
|
||||
)
|
||||
if override is not None:
|
||||
power_level_content.update(override)
|
||||
|
||||
@@ -1157,45 +1182,39 @@ class RoomCreationHandler:
|
||||
if power_level_content_override:
|
||||
power_level_content.update(power_level_content_override)
|
||||
|
||||
last_sent_stream_id = await send(
|
||||
etype=EventTypes.PowerLevels, content=power_level_content
|
||||
)
|
||||
state_to_send[(EventTypes.PowerLevels, "")] = power_level_content
|
||||
|
||||
if room_alias and (EventTypes.CanonicalAlias, "") not in initial_state:
|
||||
last_sent_stream_id = await send(
|
||||
etype=EventTypes.CanonicalAlias,
|
||||
content={"alias": room_alias.to_string()},
|
||||
)
|
||||
if room_alias:
|
||||
state_to_send[(EventTypes.CanonicalAlias, "")] = {
|
||||
"alias": room_alias.to_string()
|
||||
}
|
||||
|
||||
if (EventTypes.JoinRules, "") not in initial_state:
|
||||
last_sent_stream_id = await send(
|
||||
etype=EventTypes.JoinRules, content={"join_rule": config["join_rules"]}
|
||||
)
|
||||
state_to_send[(EventTypes.JoinRules, "")] = {
|
||||
"join_rule": room_preset_config["join_rules"]
|
||||
}
|
||||
state_to_send[(EventTypes.RoomHistoryVisibility, "")] = {
|
||||
"history_visibility": room_preset_config["history_visibility"]
|
||||
}
|
||||
|
||||
if (EventTypes.RoomHistoryVisibility, "") not in initial_state:
|
||||
last_sent_stream_id = await send(
|
||||
etype=EventTypes.RoomHistoryVisibility,
|
||||
content={"history_visibility": config["history_visibility"]},
|
||||
)
|
||||
if room_preset_config["guest_can_join"]:
|
||||
state_to_send[(EventTypes.GuestAccess, "")] = {
|
||||
EventContentFields.GUEST_ACCESS: GuestAccess.CAN_JOIN
|
||||
}
|
||||
|
||||
if config["guest_can_join"]:
|
||||
if (EventTypes.GuestAccess, "") not in initial_state:
|
||||
last_sent_stream_id = await send(
|
||||
etype=EventTypes.GuestAccess,
|
||||
content={EventContentFields.GUEST_ACCESS: GuestAccess.CAN_JOIN},
|
||||
)
|
||||
# Override any default state with the user-provided contents of `initial_state`
|
||||
state_to_send.update(initial_state)
|
||||
|
||||
for (etype, state_key), content in initial_state.items():
|
||||
last_sent_stream_id = await send(
|
||||
etype=etype, state_key=state_key, content=content
|
||||
)
|
||||
# Set the encryption state of the room based on the room preset config
|
||||
# FIXME: We should do this before layering initial_state on top, c.f.
|
||||
# https://github.com/matrix-org/synapse/issues/9794.
|
||||
if room_preset_config["encrypted"]:
|
||||
state_to_send[(EventTypes.RoomEncryption, "")] = {
|
||||
"algorithm": RoomEncryptionAlgorithms.DEFAULT
|
||||
}
|
||||
|
||||
if config["encrypted"]:
|
||||
last_sent_stream_id = await send(
|
||||
etype=EventTypes.RoomEncryption,
|
||||
state_key="",
|
||||
content={"algorithm": RoomEncryptionAlgorithms.DEFAULT},
|
||||
)
|
||||
# Send each event in order of its insertion into the dictionary
|
||||
for (event_type, state_key), content in state_to_send.items():
|
||||
await send(etype=event_type, state_key=state_key, content=content)
|
||||
|
||||
return last_sent_stream_id, last_sent_event_id, depth
|
||||
|
||||
|
||||
@@ -182,7 +182,7 @@ class RoomListHandler:
|
||||
== HistoryVisibility.WORLD_READABLE,
|
||||
"guest_can_join": room["guest_access"] == "can_join",
|
||||
"join_rule": room["join_rules"],
|
||||
"org.matrix.msc3827.room_type": room["room_type"],
|
||||
"room_type": room["room_type"],
|
||||
}
|
||||
|
||||
# Filter out Nones – rather omit the field altogether
|
||||
|
||||
@@ -32,6 +32,7 @@ from synapse.event_auth import get_named_level, get_power_level_event
|
||||
from synapse.events import EventBase
|
||||
from synapse.events.snapshot import EventContext
|
||||
from synapse.handlers.profile import MAX_AVATAR_URL_LEN, MAX_DISPLAYNAME_LEN
|
||||
from synapse.logging import opentracing
|
||||
from synapse.module_api import NOT_SPAM
|
||||
from synapse.storage.state import StateFilter
|
||||
from synapse.types import (
|
||||
@@ -428,14 +429,14 @@ class RoomMemberHandler(metaclass=abc.ABCMeta):
|
||||
await self._join_rate_per_room_limiter.ratelimit(
|
||||
requester, key=room_id, update=False
|
||||
)
|
||||
|
||||
result_event = await self.event_creation_handler.handle_new_client_event(
|
||||
requester,
|
||||
event,
|
||||
context,
|
||||
extra_users=[target],
|
||||
ratelimit=ratelimit,
|
||||
)
|
||||
with opentracing.start_active_span("handle_new_client_event"):
|
||||
result_event = await self.event_creation_handler.handle_new_client_event(
|
||||
requester,
|
||||
event,
|
||||
context,
|
||||
extra_users=[target],
|
||||
ratelimit=ratelimit,
|
||||
)
|
||||
|
||||
if event.membership == Membership.LEAVE:
|
||||
if prev_member_event_id:
|
||||
@@ -564,25 +565,26 @@ class RoomMemberHandler(metaclass=abc.ABCMeta):
|
||||
# by application services), and then by room ID.
|
||||
async with self.member_as_limiter.queue(as_id):
|
||||
async with self.member_linearizer.queue(key):
|
||||
result = await self.update_membership_locked(
|
||||
requester,
|
||||
target,
|
||||
room_id,
|
||||
action,
|
||||
txn_id=txn_id,
|
||||
remote_room_hosts=remote_room_hosts,
|
||||
third_party_signed=third_party_signed,
|
||||
ratelimit=ratelimit,
|
||||
content=content,
|
||||
new_room=new_room,
|
||||
require_consent=require_consent,
|
||||
outlier=outlier,
|
||||
historical=historical,
|
||||
allow_no_prev_events=allow_no_prev_events,
|
||||
prev_event_ids=prev_event_ids,
|
||||
state_event_ids=state_event_ids,
|
||||
depth=depth,
|
||||
)
|
||||
with opentracing.start_active_span("update_membership_locked"):
|
||||
result = await self.update_membership_locked(
|
||||
requester,
|
||||
target,
|
||||
room_id,
|
||||
action,
|
||||
txn_id=txn_id,
|
||||
remote_room_hosts=remote_room_hosts,
|
||||
third_party_signed=third_party_signed,
|
||||
ratelimit=ratelimit,
|
||||
content=content,
|
||||
new_room=new_room,
|
||||
require_consent=require_consent,
|
||||
outlier=outlier,
|
||||
historical=historical,
|
||||
allow_no_prev_events=allow_no_prev_events,
|
||||
prev_event_ids=prev_event_ids,
|
||||
state_event_ids=state_event_ids,
|
||||
depth=depth,
|
||||
)
|
||||
|
||||
return result
|
||||
|
||||
@@ -649,6 +651,7 @@ class RoomMemberHandler(metaclass=abc.ABCMeta):
|
||||
Returns:
|
||||
A tuple of the new event ID and stream ID.
|
||||
"""
|
||||
|
||||
content_specified = bool(content)
|
||||
if content is None:
|
||||
content = {}
|
||||
@@ -1679,7 +1682,11 @@ class RoomMemberMasterHandler(RoomMemberHandler):
|
||||
]
|
||||
|
||||
if len(remote_room_hosts) == 0:
|
||||
raise SynapseError(404, "No known servers")
|
||||
raise SynapseError(
|
||||
404,
|
||||
"Can't join remote room because no servers "
|
||||
"that are in the room have been provided.",
|
||||
)
|
||||
|
||||
check_complexity = self.hs.config.server.limit_remote_rooms.enabled
|
||||
if (
|
||||
|
||||
@@ -28,11 +28,11 @@ from synapse.api.constants import (
|
||||
RoomTypes,
|
||||
)
|
||||
from synapse.api.errors import (
|
||||
AuthError,
|
||||
Codes,
|
||||
NotFoundError,
|
||||
StoreError,
|
||||
SynapseError,
|
||||
UnstableSpecAuthError,
|
||||
UnsupportedRoomVersionError,
|
||||
)
|
||||
from synapse.api.ratelimiting import Ratelimiter
|
||||
@@ -175,10 +175,11 @@ class RoomSummaryHandler:
|
||||
|
||||
# First of all, check that the room is accessible.
|
||||
if not await self._is_local_room_accessible(requested_room_id, requester):
|
||||
raise AuthError(
|
||||
raise UnstableSpecAuthError(
|
||||
403,
|
||||
"User %s not in room %s, and room previews are disabled"
|
||||
% (requester, requested_room_id),
|
||||
errcode=Codes.NOT_JOINED,
|
||||
)
|
||||
|
||||
# If this is continuing a previous session, pull the persisted data.
|
||||
|
||||
+93
-48
@@ -13,7 +13,17 @@
|
||||
# limitations under the License.
|
||||
import itertools
|
||||
import logging
|
||||
from typing import TYPE_CHECKING, Any, Dict, FrozenSet, List, Optional, Set, Tuple
|
||||
from typing import (
|
||||
TYPE_CHECKING,
|
||||
Any,
|
||||
Dict,
|
||||
FrozenSet,
|
||||
List,
|
||||
Optional,
|
||||
Sequence,
|
||||
Set,
|
||||
Tuple,
|
||||
)
|
||||
|
||||
import attr
|
||||
from prometheus_client import Counter
|
||||
@@ -89,7 +99,7 @@ class SyncConfig:
|
||||
@attr.s(slots=True, frozen=True, auto_attribs=True)
|
||||
class TimelineBatch:
|
||||
prev_batch: StreamToken
|
||||
events: List[EventBase]
|
||||
events: Sequence[EventBase]
|
||||
limited: bool
|
||||
# A mapping of event ID to the bundled aggregations for the above events.
|
||||
# This is only calculated if limited is true.
|
||||
@@ -852,16 +862,26 @@ class SyncHandler:
|
||||
now_token: StreamToken,
|
||||
full_state: bool,
|
||||
) -> MutableStateMap[EventBase]:
|
||||
"""Works out the difference in state between the start of the timeline
|
||||
and the previous sync.
|
||||
"""Works out the difference in state between the end of the previous sync and
|
||||
the start of the timeline.
|
||||
|
||||
Args:
|
||||
room_id:
|
||||
batch: The timeline batch for the room that will be sent to the user.
|
||||
sync_config:
|
||||
since_token: Token of the end of the previous batch. May be None.
|
||||
since_token: Token of the end of the previous batch. May be `None`.
|
||||
now_token: Token of the end of the current batch.
|
||||
full_state: Whether to force returning the full state.
|
||||
`lazy_load_members` still applies when `full_state` is `True`.
|
||||
|
||||
Returns:
|
||||
The state to return in the sync response for the room.
|
||||
|
||||
Clients will overlay this onto the state at the end of the previous sync to
|
||||
arrive at the state at the start of the timeline.
|
||||
|
||||
Clients will then overlay state events in the timeline to arrive at the
|
||||
state at the end of the timeline, in preparation for the next sync.
|
||||
"""
|
||||
# TODO(mjark) Check if the state events were received by the server
|
||||
# after the previous sync, since we need to include those state
|
||||
@@ -869,7 +889,8 @@ class SyncHandler:
|
||||
# TODO(mjark) Check for new redactions in the state events.
|
||||
|
||||
with Measure(self.clock, "compute_state_delta"):
|
||||
|
||||
# The memberships needed for events in the timeline.
|
||||
# Only calculated when `lazy_load_members` is on.
|
||||
members_to_fetch = None
|
||||
|
||||
lazy_load_members = sync_config.filter_collection.lazy_load_members()
|
||||
@@ -897,38 +918,46 @@ class SyncHandler:
|
||||
else:
|
||||
state_filter = StateFilter.all()
|
||||
|
||||
# The contribution to the room state from state events in the timeline.
|
||||
# Only contains the last event for any given state key.
|
||||
timeline_state = {
|
||||
(event.type, event.state_key): event.event_id
|
||||
for event in batch.events
|
||||
if event.is_state()
|
||||
}
|
||||
|
||||
# Now calculate the state to return in the sync response for the room.
|
||||
# This is more or less the change in state between the end of the previous
|
||||
# sync's timeline and the start of the current sync's timeline.
|
||||
# See the docstring above for details.
|
||||
state_ids: StateMap[str]
|
||||
|
||||
if full_state:
|
||||
if batch:
|
||||
current_state_ids = (
|
||||
state_at_timeline_end = (
|
||||
await self._state_storage_controller.get_state_ids_for_event(
|
||||
batch.events[-1].event_id, state_filter=state_filter
|
||||
)
|
||||
)
|
||||
|
||||
state_ids = (
|
||||
state_at_timeline_start = (
|
||||
await self._state_storage_controller.get_state_ids_for_event(
|
||||
batch.events[0].event_id, state_filter=state_filter
|
||||
)
|
||||
)
|
||||
|
||||
else:
|
||||
current_state_ids = await self.get_state_at(
|
||||
state_at_timeline_end = await self.get_state_at(
|
||||
room_id, stream_position=now_token, state_filter=state_filter
|
||||
)
|
||||
|
||||
state_ids = current_state_ids
|
||||
state_at_timeline_start = state_at_timeline_end
|
||||
|
||||
state_ids = _calculate_state(
|
||||
timeline_contains=timeline_state,
|
||||
timeline_start=state_ids,
|
||||
previous={},
|
||||
current=current_state_ids,
|
||||
timeline_start=state_at_timeline_start,
|
||||
timeline_end=state_at_timeline_end,
|
||||
previous_timeline_end={},
|
||||
lazy_load_members=lazy_load_members,
|
||||
)
|
||||
elif batch.limited:
|
||||
@@ -968,24 +997,23 @@ class SyncHandler:
|
||||
)
|
||||
|
||||
if batch:
|
||||
current_state_ids = (
|
||||
state_at_timeline_end = (
|
||||
await self._state_storage_controller.get_state_ids_for_event(
|
||||
batch.events[-1].event_id, state_filter=state_filter
|
||||
)
|
||||
)
|
||||
else:
|
||||
# Its not clear how we get here, but empirically we do
|
||||
# (#5407). Logging has been added elsewhere to try and
|
||||
# figure out where this state comes from.
|
||||
current_state_ids = await self.get_state_at(
|
||||
# We can get here if the user has ignored the senders of all
|
||||
# the recent events.
|
||||
state_at_timeline_end = await self.get_state_at(
|
||||
room_id, stream_position=now_token, state_filter=state_filter
|
||||
)
|
||||
|
||||
state_ids = _calculate_state(
|
||||
timeline_contains=timeline_state,
|
||||
timeline_start=state_at_timeline_start,
|
||||
previous=state_at_previous_sync,
|
||||
current=current_state_ids,
|
||||
timeline_end=state_at_timeline_end,
|
||||
previous_timeline_end=state_at_previous_sync,
|
||||
# we have to include LL members in case LL initial sync missed them
|
||||
lazy_load_members=lazy_load_members,
|
||||
)
|
||||
@@ -1010,6 +1038,13 @@ class SyncHandler:
|
||||
),
|
||||
)
|
||||
|
||||
# At this point, if `lazy_load_members` is enabled, `state_ids` includes
|
||||
# the memberships of all event senders in the timeline. This is because we
|
||||
# may not have sent the memberships in a previous sync.
|
||||
|
||||
# When `include_redundant_members` is on, we send all the lazy-loaded
|
||||
# memberships of event senders. Otherwise we make an effort to limit the set
|
||||
# of memberships we send to those that we have not already sent to this client.
|
||||
if lazy_load_members and not include_redundant_members:
|
||||
cache_key = (sync_config.user.to_string(), sync_config.device_id)
|
||||
cache = self.get_lazy_loaded_members_cache(cache_key)
|
||||
@@ -1536,15 +1571,13 @@ class SyncHandler:
|
||||
ignored_users = await self.store.ignored_users(user_id)
|
||||
if since_token:
|
||||
room_changes = await self._get_rooms_changed(
|
||||
sync_result_builder, ignored_users, self.rooms_to_exclude
|
||||
sync_result_builder, ignored_users
|
||||
)
|
||||
tags_by_room = await self.store.get_updated_tags(
|
||||
user_id, since_token.account_data_key
|
||||
)
|
||||
else:
|
||||
room_changes = await self._get_all_rooms(
|
||||
sync_result_builder, ignored_users, self.rooms_to_exclude
|
||||
)
|
||||
room_changes = await self._get_all_rooms(sync_result_builder, ignored_users)
|
||||
tags_by_room = await self.store.get_tags_for_user(user_id)
|
||||
|
||||
log_kv({"rooms_changed": len(room_changes.room_entries)})
|
||||
@@ -1623,13 +1656,14 @@ class SyncHandler:
|
||||
self,
|
||||
sync_result_builder: "SyncResultBuilder",
|
||||
ignored_users: FrozenSet[str],
|
||||
excluded_rooms: List[str],
|
||||
) -> _RoomChanges:
|
||||
"""Determine the changes in rooms to report to the user.
|
||||
|
||||
This function is a first pass at generating the rooms part of the sync response.
|
||||
It determines which rooms have changed during the sync period, and categorises
|
||||
them into four buckets: "knock", "invite", "join" and "leave".
|
||||
them into four buckets: "knock", "invite", "join" and "leave". It also excludes
|
||||
from that list any room that appears in the list of rooms to exclude from sync
|
||||
results in the server configuration.
|
||||
|
||||
1. Finds all membership changes for the user in the sync period (from
|
||||
`since_token` up to `now_token`).
|
||||
@@ -1655,7 +1689,7 @@ class SyncHandler:
|
||||
# _have_rooms_changed. We could keep the results in memory to avoid a
|
||||
# second query, at the cost of more complicated source code.
|
||||
membership_change_events = await self.store.get_membership_changes_for_user(
|
||||
user_id, since_token.room_key, now_token.room_key, excluded_rooms
|
||||
user_id, since_token.room_key, now_token.room_key, self.rooms_to_exclude
|
||||
)
|
||||
|
||||
mem_change_events_by_room_id: Dict[str, List[EventBase]] = {}
|
||||
@@ -1862,7 +1896,6 @@ class SyncHandler:
|
||||
self,
|
||||
sync_result_builder: "SyncResultBuilder",
|
||||
ignored_users: FrozenSet[str],
|
||||
ignored_rooms: List[str],
|
||||
) -> _RoomChanges:
|
||||
"""Returns entries for all rooms for the user.
|
||||
|
||||
@@ -1884,7 +1917,7 @@ class SyncHandler:
|
||||
room_list = await self.store.get_rooms_for_local_user_where_membership_is(
|
||||
user_id=user_id,
|
||||
membership_list=Membership.LIST,
|
||||
excluded_rooms=ignored_rooms,
|
||||
excluded_rooms=self.rooms_to_exclude,
|
||||
)
|
||||
|
||||
room_entries = []
|
||||
@@ -2150,7 +2183,9 @@ class SyncHandler:
|
||||
raise Exception("Unrecognized rtype: %r", room_builder.rtype)
|
||||
|
||||
async def get_rooms_for_user_at(
|
||||
self, user_id: str, room_key: RoomStreamToken
|
||||
self,
|
||||
user_id: str,
|
||||
room_key: RoomStreamToken,
|
||||
) -> FrozenSet[str]:
|
||||
"""Get set of joined rooms for a user at the given stream ordering.
|
||||
|
||||
@@ -2176,7 +2211,12 @@ class SyncHandler:
|
||||
# If the membership's stream ordering is after the given stream
|
||||
# ordering, we need to go and work out if the user was in the room
|
||||
# before.
|
||||
# We also need to check whether the room should be excluded from sync
|
||||
# responses as per the homeserver config.
|
||||
for joined_room in joined_rooms:
|
||||
if joined_room.room_id in self.rooms_to_exclude:
|
||||
continue
|
||||
|
||||
if not joined_room.event_pos.persisted_after(room_key):
|
||||
joined_room_ids.add(joined_room.room_id)
|
||||
continue
|
||||
@@ -2211,8 +2251,8 @@ def _action_has_highlight(actions: List[JsonDict]) -> bool:
|
||||
def _calculate_state(
|
||||
timeline_contains: StateMap[str],
|
||||
timeline_start: StateMap[str],
|
||||
previous: StateMap[str],
|
||||
current: StateMap[str],
|
||||
timeline_end: StateMap[str],
|
||||
previous_timeline_end: StateMap[str],
|
||||
lazy_load_members: bool,
|
||||
) -> StateMap[str]:
|
||||
"""Works out what state to include in a sync response.
|
||||
@@ -2220,45 +2260,50 @@ def _calculate_state(
|
||||
Args:
|
||||
timeline_contains: state in the timeline
|
||||
timeline_start: state at the start of the timeline
|
||||
previous: state at the end of the previous sync (or empty dict
|
||||
timeline_end: state at the end of the timeline
|
||||
previous_timeline_end: state at the end of the previous sync (or empty dict
|
||||
if this is an initial sync)
|
||||
current: state at the end of the timeline
|
||||
lazy_load_members: whether to return members from timeline_start
|
||||
or not. assumes that timeline_start has already been filtered to
|
||||
include only the members the client needs to know about.
|
||||
"""
|
||||
event_id_to_key = {
|
||||
e: key
|
||||
for key, e in itertools.chain(
|
||||
event_id_to_state_key = {
|
||||
event_id: state_key
|
||||
for state_key, event_id in itertools.chain(
|
||||
timeline_contains.items(),
|
||||
previous.items(),
|
||||
timeline_start.items(),
|
||||
current.items(),
|
||||
timeline_end.items(),
|
||||
previous_timeline_end.items(),
|
||||
)
|
||||
}
|
||||
|
||||
c_ids = set(current.values())
|
||||
ts_ids = set(timeline_start.values())
|
||||
p_ids = set(previous.values())
|
||||
tc_ids = set(timeline_contains.values())
|
||||
timeline_end_ids = set(timeline_end.values())
|
||||
timeline_start_ids = set(timeline_start.values())
|
||||
previous_timeline_end_ids = set(previous_timeline_end.values())
|
||||
timeline_contains_ids = set(timeline_contains.values())
|
||||
|
||||
# If we are lazyloading room members, we explicitly add the membership events
|
||||
# for the senders in the timeline into the state block returned by /sync,
|
||||
# as we may not have sent them to the client before. We find these membership
|
||||
# events by filtering them out of timeline_start, which has already been filtered
|
||||
# to only include membership events for the senders in the timeline.
|
||||
# In practice, we can do this by removing them from the p_ids list,
|
||||
# which is the list of relevant state we know we have already sent to the client.
|
||||
# In practice, we can do this by removing them from the previous_timeline_end_ids
|
||||
# list, which is the list of relevant state we know we have already sent to the
|
||||
# client.
|
||||
# see https://github.com/matrix-org/synapse/pull/2970/files/efcdacad7d1b7f52f879179701c7e0d9b763511f#r204732809
|
||||
|
||||
if lazy_load_members:
|
||||
p_ids.difference_update(
|
||||
previous_timeline_end_ids.difference_update(
|
||||
e for t, e in timeline_start.items() if t[0] == EventTypes.Member
|
||||
)
|
||||
|
||||
state_ids = ((c_ids | ts_ids) - p_ids) - tc_ids
|
||||
state_ids = (
|
||||
(timeline_end_ids | timeline_start_ids)
|
||||
- previous_timeline_end_ids
|
||||
- timeline_contains_ids
|
||||
)
|
||||
|
||||
return {event_id_to_key[e]: e for e in state_ids}
|
||||
return {event_id_to_state_key[e]: e for e in state_ids}
|
||||
|
||||
|
||||
@attr.s(slots=True, auto_attribs=True)
|
||||
|
||||
@@ -489,8 +489,15 @@ class TypingNotificationEventSource(EventSource[int, JsonDict]):
|
||||
handler = self.get_typing_handler()
|
||||
|
||||
events = []
|
||||
for room_id in handler._room_serials.keys():
|
||||
if handler._room_serials[room_id] <= from_key:
|
||||
|
||||
# Work on a copy of things here as these may change in the handler while
|
||||
# waiting for the AS `is_interested_in_room` call to complete.
|
||||
# Shallow copy is safe as no nested data is present.
|
||||
latest_room_serial = handler._latest_room_serial
|
||||
room_serials = handler._room_serials.copy()
|
||||
|
||||
for room_id, serial in room_serials.items():
|
||||
if serial <= from_key:
|
||||
continue
|
||||
|
||||
if not await service.is_interested_in_room(room_id, self._main_store):
|
||||
@@ -498,7 +505,7 @@ class TypingNotificationEventSource(EventSource[int, JsonDict]):
|
||||
|
||||
events.append(self._make_event_for(room_id))
|
||||
|
||||
return events, handler._latest_room_serial
|
||||
return events, latest_room_serial
|
||||
|
||||
async def get_new_events(
|
||||
self,
|
||||
|
||||
+14
-4
@@ -58,6 +58,7 @@ from synapse.api.errors import (
|
||||
SynapseError,
|
||||
UnrecognizedRequestError,
|
||||
)
|
||||
from synapse.config.homeserver import HomeServerConfig
|
||||
from synapse.http.site import SynapseRequest
|
||||
from synapse.logging.context import defer_to_thread, preserve_fn, run_in_background
|
||||
from synapse.logging.opentracing import active_span, start_active_span, trace_servlet
|
||||
@@ -155,15 +156,16 @@ def is_method_cancellable(method: Callable[..., Any]) -> bool:
|
||||
return getattr(method, "cancellable", False)
|
||||
|
||||
|
||||
def return_json_error(f: failure.Failure, request: SynapseRequest) -> None:
|
||||
def return_json_error(
|
||||
f: failure.Failure, request: SynapseRequest, config: Optional[HomeServerConfig]
|
||||
) -> None:
|
||||
"""Sends a JSON error response to clients."""
|
||||
|
||||
if f.check(SynapseError):
|
||||
# mypy doesn't understand that f.check asserts the type.
|
||||
exc: SynapseError = f.value # type: ignore
|
||||
error_code = exc.code
|
||||
error_dict = exc.error_dict()
|
||||
|
||||
error_dict = exc.error_dict(config)
|
||||
logger.info("%s SynapseError: %s - %s", request, error_code, exc.msg)
|
||||
elif f.check(CancelledError):
|
||||
error_code = HTTP_STATUS_REQUEST_CANCELLED
|
||||
@@ -450,7 +452,7 @@ class DirectServeJsonResource(_AsyncResource):
|
||||
request: SynapseRequest,
|
||||
) -> None:
|
||||
"""Implements _AsyncResource._send_error_response"""
|
||||
return_json_error(f, request)
|
||||
return_json_error(f, request, None)
|
||||
|
||||
|
||||
@attr.s(slots=True, frozen=True, auto_attribs=True)
|
||||
@@ -575,6 +577,14 @@ class JsonResource(DirectServeJsonResource):
|
||||
|
||||
return callback_return
|
||||
|
||||
def _send_error_response(
|
||||
self,
|
||||
f: failure.Failure,
|
||||
request: SynapseRequest,
|
||||
) -> None:
|
||||
"""Implements _AsyncResource._send_error_response"""
|
||||
return_json_error(f, request, self.hs.config)
|
||||
|
||||
|
||||
class DirectServeHtmlResource(_AsyncResource):
|
||||
"""A resource that will call `self._async_on_<METHOD>` on new requests,
|
||||
|
||||
+119
-62
@@ -173,6 +173,7 @@ from typing import (
|
||||
Any,
|
||||
Callable,
|
||||
Collection,
|
||||
ContextManager,
|
||||
Dict,
|
||||
Generator,
|
||||
Iterable,
|
||||
@@ -823,75 +824,117 @@ def extract_text_map(carrier: Dict[str, str]) -> Optional["opentracing.SpanConte
|
||||
# Tracing decorators
|
||||
|
||||
|
||||
def trace_with_opname(opname: str) -> Callable[[Callable[P, R]], Callable[P, R]]:
|
||||
def _custom_sync_async_decorator(
|
||||
func: Callable[P, R],
|
||||
wrapping_logic: Callable[[Callable[P, R], Any, Any], ContextManager[None]],
|
||||
) -> Callable[P, R]:
|
||||
"""
|
||||
Decorates a function that is sync or async (coroutines), or that returns a Twisted
|
||||
`Deferred`. The custom business logic of the decorator goes in `wrapping_logic`.
|
||||
|
||||
Example usage:
|
||||
```py
|
||||
# Decorator to time the function and log it out
|
||||
def duration(func: Callable[P, R]) -> Callable[P, R]:
|
||||
@contextlib.contextmanager
|
||||
def _wrapping_logic(func: Callable[P, R], *args: P.args, **kwargs: P.kwargs) -> Generator[None, None, None]:
|
||||
start_ts = time.time()
|
||||
try:
|
||||
yield
|
||||
finally:
|
||||
end_ts = time.time()
|
||||
duration = end_ts - start_ts
|
||||
logger.info("%s took %s seconds", func.__name__, duration)
|
||||
return _custom_sync_async_decorator(func, _wrapping_logic)
|
||||
```
|
||||
|
||||
Args:
|
||||
func: The function to be decorated
|
||||
wrapping_logic: The business logic of your custom decorator.
|
||||
This should be a ContextManager so you are able to run your logic
|
||||
before/after the function as desired.
|
||||
"""
|
||||
|
||||
if inspect.iscoroutinefunction(func):
|
||||
|
||||
@wraps(func)
|
||||
async def _wrapper(*args: P.args, **kwargs: P.kwargs) -> R:
|
||||
with wrapping_logic(func, *args, **kwargs):
|
||||
return await func(*args, **kwargs) # type: ignore[misc]
|
||||
|
||||
else:
|
||||
# The other case here handles both sync functions and those
|
||||
# decorated with inlineDeferred.
|
||||
@wraps(func)
|
||||
def _wrapper(*args: P.args, **kwargs: P.kwargs) -> R:
|
||||
scope = wrapping_logic(func, *args, **kwargs)
|
||||
scope.__enter__()
|
||||
|
||||
try:
|
||||
result = func(*args, **kwargs)
|
||||
if isinstance(result, defer.Deferred):
|
||||
|
||||
def call_back(result: R) -> R:
|
||||
scope.__exit__(None, None, None)
|
||||
return result
|
||||
|
||||
def err_back(result: R) -> R:
|
||||
scope.__exit__(None, None, None)
|
||||
return result
|
||||
|
||||
result.addCallbacks(call_back, err_back)
|
||||
|
||||
else:
|
||||
if inspect.isawaitable(result):
|
||||
logger.error(
|
||||
"@trace may not have wrapped %s correctly! "
|
||||
"The function is not async but returned a %s.",
|
||||
func.__qualname__,
|
||||
type(result).__name__,
|
||||
)
|
||||
|
||||
scope.__exit__(None, None, None)
|
||||
|
||||
return result
|
||||
|
||||
except Exception as e:
|
||||
scope.__exit__(type(e), None, e.__traceback__)
|
||||
raise
|
||||
|
||||
return _wrapper # type: ignore[return-value]
|
||||
|
||||
|
||||
def trace_with_opname(
|
||||
opname: str,
|
||||
*,
|
||||
tracer: Optional["opentracing.Tracer"] = None,
|
||||
) -> Callable[[Callable[P, R]], Callable[P, R]]:
|
||||
"""
|
||||
Decorator to trace a function with a custom opname.
|
||||
|
||||
See the module's doc string for usage examples.
|
||||
|
||||
"""
|
||||
|
||||
def decorator(func: Callable[P, R]) -> Callable[P, R]:
|
||||
if opentracing is None:
|
||||
return func # type: ignore[unreachable]
|
||||
# type-ignore: mypy bug, see https://github.com/python/mypy/issues/12909
|
||||
@contextlib.contextmanager # type: ignore[arg-type]
|
||||
def _wrapping_logic(
|
||||
func: Callable[P, R], *args: P.args, **kwargs: P.kwargs
|
||||
) -> Generator[None, None, None]:
|
||||
with start_active_span(opname, tracer=tracer):
|
||||
yield
|
||||
|
||||
if inspect.iscoroutinefunction(func):
|
||||
def _decorator(func: Callable[P, R]) -> Callable[P, R]:
|
||||
if not opentracing:
|
||||
return func
|
||||
|
||||
@wraps(func)
|
||||
async def _trace_inner(*args: P.args, **kwargs: P.kwargs) -> R:
|
||||
with start_active_span(opname):
|
||||
return await func(*args, **kwargs) # type: ignore[misc]
|
||||
return _custom_sync_async_decorator(func, _wrapping_logic)
|
||||
|
||||
else:
|
||||
# The other case here handles both sync functions and those
|
||||
# decorated with inlineDeferred.
|
||||
@wraps(func)
|
||||
def _trace_inner(*args: P.args, **kwargs: P.kwargs) -> R:
|
||||
scope = start_active_span(opname)
|
||||
scope.__enter__()
|
||||
|
||||
try:
|
||||
result = func(*args, **kwargs)
|
||||
if isinstance(result, defer.Deferred):
|
||||
|
||||
def call_back(result: R) -> R:
|
||||
scope.__exit__(None, None, None)
|
||||
return result
|
||||
|
||||
def err_back(result: R) -> R:
|
||||
scope.__exit__(None, None, None)
|
||||
return result
|
||||
|
||||
result.addCallbacks(call_back, err_back)
|
||||
|
||||
else:
|
||||
if inspect.isawaitable(result):
|
||||
logger.error(
|
||||
"@trace may not have wrapped %s correctly! "
|
||||
"The function is not async but returned a %s.",
|
||||
func.__qualname__,
|
||||
type(result).__name__,
|
||||
)
|
||||
|
||||
scope.__exit__(None, None, None)
|
||||
|
||||
return result
|
||||
|
||||
except Exception as e:
|
||||
scope.__exit__(type(e), None, e.__traceback__)
|
||||
raise
|
||||
|
||||
return _trace_inner # type: ignore[return-value]
|
||||
|
||||
return decorator
|
||||
return _decorator
|
||||
|
||||
|
||||
def trace(func: Callable[P, R]) -> Callable[P, R]:
|
||||
"""
|
||||
Decorator to trace a function.
|
||||
|
||||
Sets the operation name to that of the function's name.
|
||||
|
||||
See the module's doc string for usage examples.
|
||||
"""
|
||||
|
||||
@@ -900,22 +943,36 @@ def trace(func: Callable[P, R]) -> Callable[P, R]:
|
||||
|
||||
def tag_args(func: Callable[P, R]) -> Callable[P, R]:
|
||||
"""
|
||||
Tags all of the args to the active span.
|
||||
Decorator to tag all of the args to the active span.
|
||||
|
||||
Args:
|
||||
func: `func` is assumed to be a method taking a `self` parameter, or a
|
||||
`classmethod` taking a `cls` parameter. In either case, a tag is not
|
||||
created for this parameter.
|
||||
"""
|
||||
|
||||
if not opentracing:
|
||||
return func
|
||||
|
||||
@wraps(func)
|
||||
def _tag_args_inner(*args: P.args, **kwargs: P.kwargs) -> R:
|
||||
# type-ignore: mypy bug, see https://github.com/python/mypy/issues/12909
|
||||
@contextlib.contextmanager # type: ignore[arg-type]
|
||||
def _wrapping_logic(
|
||||
func: Callable[P, R], *args: P.args, **kwargs: P.kwargs
|
||||
) -> Generator[None, None, None]:
|
||||
argspec = inspect.getfullargspec(func)
|
||||
for i, arg in enumerate(argspec.args[1:]):
|
||||
set_tag("ARG_" + arg, str(args[i])) # type: ignore[index]
|
||||
# We use `[1:]` to skip the `self` object reference and `start=1` to
|
||||
# make the index line up with `argspec.args`.
|
||||
#
|
||||
# FIXME: We could update this to handle any type of function by ignoring the
|
||||
# first argument only if it's named `self` or `cls`. This isn't fool-proof
|
||||
# but handles the idiomatic cases.
|
||||
for i, arg in enumerate(args[1:], start=1): # type: ignore[index]
|
||||
set_tag("ARG_" + argspec.args[i], str(arg))
|
||||
set_tag("args", str(args[len(argspec.args) :])) # type: ignore[index]
|
||||
set_tag("kwargs", str(kwargs))
|
||||
return func(*args, **kwargs)
|
||||
yield
|
||||
|
||||
return _tag_args_inner
|
||||
return _custom_sync_async_decorator(func, _wrapping_logic)
|
||||
|
||||
|
||||
@contextlib.contextmanager
|
||||
|
||||
@@ -929,10 +929,12 @@ class ModuleApi:
|
||||
room_id: str,
|
||||
new_membership: str,
|
||||
content: Optional[JsonDict] = None,
|
||||
remote_room_hosts: Optional[List[str]] = None,
|
||||
) -> EventBase:
|
||||
"""Updates the membership of a user to the given value.
|
||||
|
||||
Added in Synapse v1.46.0.
|
||||
Changed in Synapse v1.65.0: Added the 'remote_room_hosts' parameter.
|
||||
|
||||
Args:
|
||||
sender: The user performing the membership change. Must be a user local to
|
||||
@@ -946,6 +948,7 @@ class ModuleApi:
|
||||
https://spec.matrix.org/unstable/client-server-api/#mroommember for the
|
||||
list of allowed values.
|
||||
content: Additional values to include in the resulting event's content.
|
||||
remote_room_hosts: Remote servers to use for remote joins/knocks/etc.
|
||||
|
||||
Returns:
|
||||
The newly created membership event.
|
||||
@@ -1005,15 +1008,12 @@ class ModuleApi:
|
||||
room_id=room_id,
|
||||
action=new_membership,
|
||||
content=content,
|
||||
remote_room_hosts=remote_room_hosts,
|
||||
)
|
||||
|
||||
# Try to retrieve the resulting event.
|
||||
event = await self._hs.get_datastores().main.get_event(event_id)
|
||||
|
||||
# update_membership is supposed to always return after the event has been
|
||||
# successfully persisted.
|
||||
assert event is not None
|
||||
|
||||
return event
|
||||
|
||||
async def create_and_send_event_into_room(self, event_dict: JsonDict) -> EventBase:
|
||||
@@ -1452,6 +1452,81 @@ class ModuleApi:
|
||||
start_timestamp, end_timestamp
|
||||
)
|
||||
|
||||
async def lookup_room_alias(self, room_alias: str) -> Tuple[str, List[str]]:
|
||||
"""
|
||||
Get the room ID associated with a room alias.
|
||||
|
||||
Added in Synapse v1.65.0.
|
||||
|
||||
Args:
|
||||
room_alias: The alias to look up.
|
||||
|
||||
Returns:
|
||||
A tuple of:
|
||||
The room ID (str).
|
||||
Hosts likely to be participating in the room ([str]).
|
||||
|
||||
Raises:
|
||||
SynapseError if room alias is invalid or could not be found.
|
||||
"""
|
||||
alias = RoomAlias.from_string(room_alias)
|
||||
(room_id, hosts) = await self._hs.get_room_member_handler().lookup_room_alias(
|
||||
alias
|
||||
)
|
||||
|
||||
return room_id.to_string(), hosts
|
||||
|
||||
async def create_room(
|
||||
self,
|
||||
user_id: str,
|
||||
config: JsonDict,
|
||||
ratelimit: bool = True,
|
||||
creator_join_profile: Optional[JsonDict] = None,
|
||||
) -> Tuple[str, Optional[str]]:
|
||||
"""Creates a new room.
|
||||
|
||||
Added in Synapse v1.65.0.
|
||||
|
||||
Args:
|
||||
user_id:
|
||||
The user who requested the room creation.
|
||||
config : A dict of configuration options. See "Request body" of:
|
||||
https://spec.matrix.org/latest/client-server-api/#post_matrixclientv3createroom
|
||||
ratelimit: set to False to disable the rate limiter for this specific operation.
|
||||
|
||||
creator_join_profile:
|
||||
Set to override the displayname and avatar for the creating
|
||||
user in this room. If unset, displayname and avatar will be
|
||||
derived from the user's profile. If set, should contain the
|
||||
values to go in the body of the 'join' event (typically
|
||||
`avatar_url` and/or `displayname`.
|
||||
|
||||
Returns:
|
||||
A tuple containing: 1) the room ID (str), 2) if an alias was requested,
|
||||
the room alias (str), otherwise None if no alias was requested.
|
||||
|
||||
Raises:
|
||||
ResourceLimitError if server is blocked to some resource being
|
||||
exceeded.
|
||||
RuntimeError if the user_id does not refer to a local user.
|
||||
SynapseError if the user_id is invalid, room ID couldn't be stored, or
|
||||
something went horribly wrong.
|
||||
"""
|
||||
if not self.is_mine(user_id):
|
||||
raise RuntimeError(
|
||||
"Tried to create a room as a user that isn't local to this homeserver",
|
||||
)
|
||||
|
||||
requester = create_requester(user_id)
|
||||
room_id_and_alias, _ = await self._hs.get_room_creation_handler().create_room(
|
||||
requester=requester,
|
||||
config=config,
|
||||
ratelimit=ratelimit,
|
||||
creator_join_profile=creator_join_profile,
|
||||
)
|
||||
|
||||
return room_id_and_alias["room_id"], room_id_and_alias.get("room_alias", None)
|
||||
|
||||
|
||||
class PublicRoomListManager:
|
||||
"""Contains methods for adding to, removing from and querying whether a room
|
||||
|
||||
@@ -416,7 +416,10 @@ class FederationSenderHandler:
|
||||
if not self._is_mine_id(receipt.user_id):
|
||||
continue
|
||||
# Private read receipts never get sent over federation.
|
||||
if receipt.receipt_type == ReceiptTypes.READ_PRIVATE:
|
||||
if receipt.receipt_type in (
|
||||
ReceiptTypes.READ_PRIVATE,
|
||||
ReceiptTypes.UNSTABLE_READ_PRIVATE,
|
||||
):
|
||||
continue
|
||||
receipt_info = ReadReceipt(
|
||||
receipt.room_id,
|
||||
|
||||
@@ -1 +1,12 @@
|
||||
<html><body>Your account is valid until {{ expiration_ts|format_ts("%d-%m-%Y") }}.</body><html>
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Your account is valid until {{ expiration_ts|format_ts("%d-%m-%Y") }}.</title>
|
||||
</head>
|
||||
<body>
|
||||
Your account is valid until {{ expiration_ts|format_ts("%d-%m-%Y") }}.
|
||||
</body>
|
||||
</html>
|
||||
@@ -1 +1,12 @@
|
||||
<html><body>Your account has been successfully renewed and is valid until {{ expiration_ts|format_ts("%d-%m-%Y") }}.</body><html>
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Your account has been successfully renewed and is valid until {{ expiration_ts|format_ts("%d-%m-%Y") }}.</title>
|
||||
</head>
|
||||
<body>
|
||||
Your account has been successfully renewed and is valid until {{ expiration_ts|format_ts("%d-%m-%Y") }}.
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,9 +1,14 @@
|
||||
<html>
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Request to add an email address to your Matrix account</title>
|
||||
</head>
|
||||
<body>
|
||||
<p>A request to add an email address to your Matrix account has been received. If this was you, please click the link below to confirm adding this email:</p>
|
||||
|
||||
<a href="{{ link }}">{{ link }}</a>
|
||||
|
||||
<p>If this was not you, you can safely ignore this email. Thank you.</p>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -1,8 +1,13 @@
|
||||
<html>
|
||||
<head></head>
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Request failed</title>
|
||||
</head>
|
||||
<body>
|
||||
<p>The request failed for the following reason: {{ failure_reason }}.</p>
|
||||
|
||||
<p>No changes have been made to your account.</p>
|
||||
<p>The request failed for the following reason: {{ failure_reason }}.</p>
|
||||
<p>No changes have been made to your account.</p>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -1,6 +1,12 @@
|
||||
<html>
|
||||
<head></head>
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Your email has now been validated</title>
|
||||
</head>
|
||||
<body>
|
||||
<p>Your email has now been validated, please return to your client. You may now close this window.</p>
|
||||
<p>Your email has now been validated, please return to your client. You may now close this window.</p>
|
||||
</body>
|
||||
</html>
|
||||
</html>
|
||||
@@ -1,8 +1,8 @@
|
||||
<html>
|
||||
<head>
|
||||
<title>Success!</title>
|
||||
<meta name='viewport' content='width=device-width, initial-scale=1,
|
||||
user-scalable=no, minimum-scale=1.0, maximum-scale=1.0'>
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<link rel="stylesheet" href="/_matrix/static/client/register/style.css">
|
||||
<script>
|
||||
if (window.onAuthDone) {
|
||||
|
||||
@@ -1 +1,12 @@
|
||||
<html><body>Invalid renewal token.</body><html>
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Invalid renewal token.</title>
|
||||
</head>
|
||||
<body>
|
||||
Invalid renewal token.
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<style type="text/css">
|
||||
{% include 'mail.css' without context %}
|
||||
{% include "mail-%s.css" % app_name ignore missing without context %}
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<style type="text/css">
|
||||
{%- include 'mail.css' without context %}
|
||||
{%- include "mail-%s.css" % app_name ignore missing without context %}
|
||||
|
||||
@@ -1,4 +1,9 @@
|
||||
<html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<title>Password reset</title>
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
</head>
|
||||
<body>
|
||||
<p>A password reset request has been received for your Matrix account. If this was you, please click the link below to confirm resetting your password:</p>
|
||||
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
<html>
|
||||
<head></head>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<title>Password reset confirmation</title>
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
</head>
|
||||
<body>
|
||||
<!--Use a hidden form to resubmit the information necessary to reset the password-->
|
||||
<form method="post">
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
<html>
|
||||
<head></head>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<title>Password reset failure</title>
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
</head>
|
||||
<body>
|
||||
<p>The request failed for the following reason: {{ failure_reason }}.</p>
|
||||
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
<html>
|
||||
<head></head>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
</head>
|
||||
<body>
|
||||
<p>Your email has now been validated, please return to your client to reset your password. You may now close this window.</p>
|
||||
</body>
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
<html>
|
||||
<head>
|
||||
<title>Authentication</title>
|
||||
<meta name='viewport' content='width=device-width, initial-scale=1,
|
||||
user-scalable=no, minimum-scale=1.0, maximum-scale=1.0'>
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<script src="https://www.recaptcha.net/recaptcha/api.js"
|
||||
async defer></script>
|
||||
<script src="//code.jquery.com/jquery-1.11.2.min.js"></script>
|
||||
|
||||
@@ -1,4 +1,9 @@
|
||||
<html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<title>Registration</title>
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
</head>
|
||||
<body>
|
||||
<p>You have asked us to register this email with a new Matrix account. If this was you, please click the link below to confirm your email address:</p>
|
||||
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
<html>
|
||||
<head></head>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
</head>
|
||||
<body>
|
||||
<p>Validation failed for the following reason: {{ failure_reason }}.</p>
|
||||
</body>
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
<html>
|
||||
<head></head>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<title>Your email has now been validated</title>
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
</head>
|
||||
<body>
|
||||
<p>Your email has now been validated, please return to your client. You may now close this window.</p>
|
||||
</body>
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
<html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<title>Authentication</title>
|
||||
<meta name='viewport' content='width=device-width, initial-scale=1,
|
||||
user-scalable=no, minimum-scale=1.0, maximum-scale=1.0'>
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<link rel="stylesheet" href="/_matrix/static/client/register/style.css">
|
||||
</head>
|
||||
<body>
|
||||
|
||||
@@ -3,8 +3,8 @@
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>SSO account deactivated</title>
|
||||
<meta name="viewport" content="width=device-width, user-scalable=no">
|
||||
<style type="text/css">
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <style type="text/css">
|
||||
{% include "sso.css" without context %}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
@@ -3,7 +3,8 @@
|
||||
<head>
|
||||
<title>Create your account</title>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, user-scalable=no">
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<script type="text/javascript">
|
||||
let wasKeyboard = false;
|
||||
document.addEventListener("mousedown", function() { wasKeyboard = false; });
|
||||
|
||||
@@ -3,7 +3,8 @@
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Authentication failed</title>
|
||||
<meta name="viewport" content="width=device-width, user-scalable=no">
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<style type="text/css">
|
||||
{% include "sso.css" without context %}
|
||||
</style>
|
||||
|
||||
@@ -3,7 +3,8 @@
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Confirm it's you</title>
|
||||
<meta name="viewport" content="width=device-width, user-scalable=no">
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<style type="text/css">
|
||||
{% include "sso.css" without context %}
|
||||
</style>
|
||||
|
||||
@@ -3,7 +3,8 @@
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Authentication successful</title>
|
||||
<meta name="viewport" content="width=device-width, user-scalable=no">
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<style type="text/css">
|
||||
{% include "sso.css" without context %}
|
||||
</style>
|
||||
|
||||
@@ -3,7 +3,8 @@
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Authentication failed</title>
|
||||
<meta name="viewport" content="width=device-width, user-scalable=no">
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<style type="text/css">
|
||||
{% include "sso.css" without context %}
|
||||
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<meta charset="UTF-8">
|
||||
<title>Choose identity provider</title>
|
||||
<style type="text/css">
|
||||
|
||||
@@ -3,7 +3,8 @@
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Agree to terms and conditions</title>
|
||||
<meta name="viewport" content="width=device-width, user-scalable=no">
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<style type="text/css">
|
||||
{% include "sso.css" without context %}
|
||||
|
||||
|
||||
@@ -3,7 +3,8 @@
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Continue to your account</title>
|
||||
<meta name="viewport" content="width=device-width, user-scalable=no">
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<style type="text/css">
|
||||
{% include "sso.css" without context %}
|
||||
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
<html>
|
||||
<head>
|
||||
<title>Authentication</title>
|
||||
<meta name='viewport' content='width=device-width, initial-scale=1,
|
||||
user-scalable=no, minimum-scale=1.0, maximum-scale=1.0'>
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<link rel="stylesheet" href="/_matrix/static/client/register/style.css">
|
||||
</head>
|
||||
<body>
|
||||
|
||||
@@ -58,7 +58,12 @@ class NotificationsServlet(RestServlet):
|
||||
)
|
||||
|
||||
receipts_by_room = await self.store.get_receipts_for_user_with_orderings(
|
||||
user_id, [ReceiptTypes.READ, ReceiptTypes.READ_PRIVATE]
|
||||
user_id,
|
||||
[
|
||||
ReceiptTypes.READ,
|
||||
ReceiptTypes.READ_PRIVATE,
|
||||
ReceiptTypes.UNSTABLE_READ_PRIVATE,
|
||||
],
|
||||
)
|
||||
|
||||
notif_event_ids = [pa.event_id for pa in push_actions]
|
||||
|
||||
@@ -40,9 +40,13 @@ class ReadMarkerRestServlet(RestServlet):
|
||||
self.read_marker_handler = hs.get_read_marker_handler()
|
||||
self.presence_handler = hs.get_presence_handler()
|
||||
|
||||
self._known_receipt_types = {ReceiptTypes.READ, ReceiptTypes.FULLY_READ}
|
||||
self._known_receipt_types = {
|
||||
ReceiptTypes.READ,
|
||||
ReceiptTypes.FULLY_READ,
|
||||
ReceiptTypes.READ_PRIVATE,
|
||||
}
|
||||
if hs.config.experimental.msc2285_enabled:
|
||||
self._known_receipt_types.add(ReceiptTypes.READ_PRIVATE)
|
||||
self._known_receipt_types.add(ReceiptTypes.UNSTABLE_READ_PRIVATE)
|
||||
|
||||
async def on_POST(
|
||||
self, request: SynapseRequest, room_id: str
|
||||
|
||||
@@ -44,11 +44,13 @@ class ReceiptRestServlet(RestServlet):
|
||||
self.read_marker_handler = hs.get_read_marker_handler()
|
||||
self.presence_handler = hs.get_presence_handler()
|
||||
|
||||
self._known_receipt_types = {ReceiptTypes.READ}
|
||||
self._known_receipt_types = {
|
||||
ReceiptTypes.READ,
|
||||
ReceiptTypes.READ_PRIVATE,
|
||||
ReceiptTypes.FULLY_READ,
|
||||
}
|
||||
if hs.config.experimental.msc2285_enabled:
|
||||
self._known_receipt_types.update(
|
||||
(ReceiptTypes.READ_PRIVATE, ReceiptTypes.FULLY_READ)
|
||||
)
|
||||
self._known_receipt_types.add(ReceiptTypes.UNSTABLE_READ_PRIVATE)
|
||||
|
||||
async def on_POST(
|
||||
self, request: SynapseRequest, room_id: str, receipt_type: str, event_id: str
|
||||
|
||||
@@ -33,7 +33,7 @@ from synapse.api.ratelimiting import Ratelimiter
|
||||
from synapse.config import ConfigError
|
||||
from synapse.config.emailconfig import ThreepidBehaviour
|
||||
from synapse.config.homeserver import HomeServerConfig
|
||||
from synapse.config.ratelimiting import FederationRateLimitConfig
|
||||
from synapse.config.ratelimiting import FederationRatelimitSettings
|
||||
from synapse.config.server import is_threepid_reserved
|
||||
from synapse.handlers.auth import AuthHandler
|
||||
from synapse.handlers.ui_auth import UIAuthSessionDataConstants
|
||||
@@ -325,7 +325,7 @@ class UsernameAvailabilityRestServlet(RestServlet):
|
||||
self.registration_handler = hs.get_registration_handler()
|
||||
self.ratelimiter = FederationRateLimiter(
|
||||
hs.get_clock(),
|
||||
FederationRateLimitConfig(
|
||||
FederationRatelimitSettings(
|
||||
# Time window of 2s
|
||||
window_size=2000,
|
||||
# Artificially delay requests if rate > sleep_limit/window_size
|
||||
|
||||
@@ -94,9 +94,10 @@ class VersionsRestServlet(RestServlet):
|
||||
# Supports the busy presence state described in MSC3026.
|
||||
"org.matrix.msc3026.busy_presence": self.config.experimental.msc3026_enabled,
|
||||
# Supports receiving private read receipts as per MSC2285
|
||||
"org.matrix.msc2285.stable": True, # TODO: Remove when MSC2285 becomes a part of the spec
|
||||
"org.matrix.msc2285": self.config.experimental.msc2285_enabled,
|
||||
# Supports filtering of /publicRooms by room type MSC3827
|
||||
"org.matrix.msc3827": self.config.experimental.msc3827_enabled,
|
||||
# Supports filtering of /publicRooms by room type as per MSC3827
|
||||
"org.matrix.msc3827.stable": True,
|
||||
# Adds support for importing historical messages as per MSC2716
|
||||
"org.matrix.msc2716": self.config.experimental.msc2716_enabled,
|
||||
# Adds support for jump to date endpoints (/timestamp_to_event) as per MSC3030
|
||||
|
||||
@@ -255,7 +255,7 @@ class StateHandler:
|
||||
self,
|
||||
event: EventBase,
|
||||
state_ids_before_event: Optional[StateMap[str]] = None,
|
||||
partial_state: bool = False,
|
||||
partial_state: Optional[bool] = None,
|
||||
) -> EventContext:
|
||||
"""Build an EventContext structure for a non-outlier event.
|
||||
|
||||
@@ -270,10 +270,18 @@ class StateHandler:
|
||||
it can't be calculated from existing events. This is normally
|
||||
only specified when receiving an event from federation where we
|
||||
don't have the prev events, e.g. when backfilling.
|
||||
partial_state: True if `state_ids_before_event` is partial and omits
|
||||
non-critical membership events
|
||||
partial_state:
|
||||
`True` if `state_ids_before_event` is partial and omits non-critical
|
||||
membership events.
|
||||
`False` if `state_ids_before_event` is the full state.
|
||||
`None` when `state_ids_before_event` is not provided. In this case, the
|
||||
flag will be calculated based on `event`'s prev events.
|
||||
Returns:
|
||||
The event context.
|
||||
|
||||
Raises:
|
||||
RuntimeError if `state_ids_before_event` is not provided and one or more
|
||||
prev events are missing or outliers.
|
||||
"""
|
||||
|
||||
assert not event.internal_metadata.is_outlier()
|
||||
@@ -298,12 +306,14 @@ class StateHandler:
|
||||
)
|
||||
)
|
||||
|
||||
# the partial_state flag must be provided
|
||||
assert partial_state is not None
|
||||
else:
|
||||
# otherwise, we'll need to resolve the state across the prev_events.
|
||||
|
||||
# partial_state should not be set explicitly in this case:
|
||||
# we work it out dynamically
|
||||
assert not partial_state
|
||||
assert partial_state is None
|
||||
|
||||
# if any of the prev-events have partial state, so do we.
|
||||
# (This is slightly racy - the prev-events might get fixed up before we use
|
||||
@@ -313,13 +323,13 @@ class StateHandler:
|
||||
incomplete_prev_events = await self.store.get_partial_state_events(
|
||||
prev_event_ids
|
||||
)
|
||||
if any(incomplete_prev_events.values()):
|
||||
partial_state = any(incomplete_prev_events.values())
|
||||
if partial_state:
|
||||
logger.debug(
|
||||
"New/incoming event %s refers to prev_events %s with partial state",
|
||||
event.event_id,
|
||||
[k for (k, v) in incomplete_prev_events.items() if v],
|
||||
)
|
||||
partial_state = True
|
||||
|
||||
logger.debug("calling resolve_state_groups from compute_event_context")
|
||||
# we've already taken into account partial state, so no need to wait for
|
||||
@@ -426,6 +436,10 @@ class StateHandler:
|
||||
|
||||
Returns:
|
||||
The resolved state
|
||||
|
||||
Raises:
|
||||
RuntimeError if we don't have a state group for one or more of the events
|
||||
(ie. they are outliers or unknown)
|
||||
"""
|
||||
logger.debug("resolve_state_groups event_ids %s", event_ids)
|
||||
|
||||
|
||||
+6
-6
@@ -434,7 +434,7 @@ async def _add_event_and_auth_chain_to_graph(
|
||||
event_id: str,
|
||||
event_map: Dict[str, EventBase],
|
||||
state_res_store: StateResolutionStore,
|
||||
auth_diff: Set[str],
|
||||
full_conflicted_set: Set[str],
|
||||
) -> None:
|
||||
"""Helper function for _reverse_topological_power_sort that add the event
|
||||
and its auth chain (that is in the auth diff) to the graph
|
||||
@@ -445,7 +445,7 @@ async def _add_event_and_auth_chain_to_graph(
|
||||
event_id: Event to add to the graph
|
||||
event_map
|
||||
state_res_store
|
||||
auth_diff: Set of event IDs that are in the auth difference.
|
||||
full_conflicted_set: Set of event IDs that are in the full conflicted set.
|
||||
"""
|
||||
|
||||
state = [event_id]
|
||||
@@ -455,7 +455,7 @@ async def _add_event_and_auth_chain_to_graph(
|
||||
|
||||
event = await _get_event(room_id, eid, event_map, state_res_store)
|
||||
for aid in event.auth_event_ids():
|
||||
if aid in auth_diff:
|
||||
if aid in full_conflicted_set:
|
||||
if aid not in graph:
|
||||
state.append(aid)
|
||||
|
||||
@@ -468,7 +468,7 @@ async def _reverse_topological_power_sort(
|
||||
event_ids: Iterable[str],
|
||||
event_map: Dict[str, EventBase],
|
||||
state_res_store: StateResolutionStore,
|
||||
auth_diff: Set[str],
|
||||
full_conflicted_set: Set[str],
|
||||
) -> List[str]:
|
||||
"""Returns a list of the event_ids sorted by reverse topological ordering,
|
||||
and then by power level and origin_server_ts
|
||||
@@ -479,7 +479,7 @@ async def _reverse_topological_power_sort(
|
||||
event_ids: The events to sort
|
||||
event_map
|
||||
state_res_store
|
||||
auth_diff: Set of event IDs that are in the auth difference.
|
||||
full_conflicted_set: Set of event IDs that are in the full conflicted set.
|
||||
|
||||
Returns:
|
||||
The sorted list
|
||||
@@ -488,7 +488,7 @@ async def _reverse_topological_power_sort(
|
||||
graph: Dict[str, Set[str]] = {}
|
||||
for idx, event_id in enumerate(event_ids, start=1):
|
||||
await _add_event_and_auth_chain_to_graph(
|
||||
graph, room_id, event_id, event_map, state_res_store, auth_diff
|
||||
graph, room_id, event_id, event_map, state_res_store, full_conflicted_set
|
||||
)
|
||||
|
||||
# We await occasionally when we're working with large data sets to
|
||||
|
||||
@@ -3,7 +3,8 @@
|
||||
<head>
|
||||
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
|
||||
<title> Login </title>
|
||||
<meta name='viewport' content='width=device-width, initial-scale=1, user-scalable=no, minimum-scale=1.0, maximum-scale=1.0'>
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<link rel="stylesheet" href="style.css">
|
||||
<script src="js/jquery-3.4.1.min.js"></script>
|
||||
<script src="js/login.js"></script>
|
||||
|
||||
@@ -2,7 +2,8 @@
|
||||
<html>
|
||||
<head>
|
||||
<title> Registration </title>
|
||||
<meta name='viewport' content='width=device-width, initial-scale=1, user-scalable=no, minimum-scale=1.0, maximum-scale=1.0'>
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<link rel="stylesheet" href="style.css">
|
||||
<script src="js/jquery-3.4.1.min.js"></script>
|
||||
<script src="https://www.recaptcha.net/recaptcha/api/js/recaptcha_ajax.js"></script>
|
||||
|
||||
@@ -29,6 +29,7 @@ from typing import (
|
||||
|
||||
from synapse.api.constants import EventTypes
|
||||
from synapse.events import EventBase
|
||||
from synapse.logging.opentracing import trace
|
||||
from synapse.storage.state import StateFilter
|
||||
from synapse.storage.util.partial_state_events_tracker import (
|
||||
PartialCurrentStateTracker,
|
||||
@@ -82,13 +83,15 @@ class StateStorageController:
|
||||
return state_group_delta.prev_group, state_group_delta.delta_ids
|
||||
|
||||
async def get_state_groups_ids(
|
||||
self, _room_id: str, event_ids: Collection[str]
|
||||
self, _room_id: str, event_ids: Collection[str], await_full_state: bool = True
|
||||
) -> Dict[int, MutableStateMap[str]]:
|
||||
"""Get the event IDs of all the state for the state groups for the given events
|
||||
|
||||
Args:
|
||||
_room_id: id of the room for these events
|
||||
event_ids: ids of the events
|
||||
await_full_state: if `True`, will block if we do not yet have complete
|
||||
state at these events.
|
||||
|
||||
Returns:
|
||||
dict of state_group_id -> (dict of (type, state_key) -> event id)
|
||||
@@ -100,7 +103,9 @@ class StateStorageController:
|
||||
if not event_ids:
|
||||
return {}
|
||||
|
||||
event_to_groups = await self.get_state_group_for_events(event_ids)
|
||||
event_to_groups = await self.get_state_group_for_events(
|
||||
event_ids, await_full_state=await_full_state
|
||||
)
|
||||
|
||||
groups = set(event_to_groups.values())
|
||||
group_to_state = await self.stores.state._get_state_for_groups(groups)
|
||||
@@ -175,6 +180,7 @@ class StateStorageController:
|
||||
|
||||
return self.stores.state._get_state_groups_from_groups(groups, state_filter)
|
||||
|
||||
@trace
|
||||
async def get_state_for_events(
|
||||
self, event_ids: Collection[str], state_filter: Optional[StateFilter] = None
|
||||
) -> Dict[str, StateMap[EventBase]]:
|
||||
@@ -221,6 +227,7 @@ class StateStorageController:
|
||||
|
||||
return {event: event_to_state[event] for event in event_ids}
|
||||
|
||||
@trace
|
||||
async def get_state_ids_for_events(
|
||||
self,
|
||||
event_ids: Collection[str],
|
||||
@@ -283,6 +290,7 @@ class StateStorageController:
|
||||
)
|
||||
return state_map[event_id]
|
||||
|
||||
@trace
|
||||
async def get_state_ids_for_event(
|
||||
self, event_id: str, state_filter: Optional[StateFilter] = None
|
||||
) -> StateMap[str]:
|
||||
@@ -323,6 +331,7 @@ class StateStorageController:
|
||||
groups, state_filter or StateFilter.all()
|
||||
)
|
||||
|
||||
@trace
|
||||
async def get_state_group_for_events(
|
||||
self,
|
||||
event_ids: Collection[str],
|
||||
@@ -334,6 +343,10 @@ class StateStorageController:
|
||||
event_ids: events to get state groups for
|
||||
await_full_state: if true, will block if we do not yet have complete
|
||||
state at these events.
|
||||
|
||||
Raises:
|
||||
RuntimeError if we don't have a state group for one or more of the events
|
||||
(ie. they are outliers or unknown)
|
||||
"""
|
||||
if await_full_state:
|
||||
await self._partial_state_events_tracker.await_full_state(event_ids)
|
||||
|
||||
@@ -12,6 +12,67 @@
|
||||
# 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.
|
||||
|
||||
"""Responsible for storing and fetching push actions / notifications.
|
||||
|
||||
There are two main uses for push actions:
|
||||
1. Sending out push to a user's device; and
|
||||
2. Tracking per-room per-user notification counts (used in sync requests).
|
||||
|
||||
For the former we simply use the `event_push_actions` table, which contains all
|
||||
the calculated actions for a given user (which were calculated by the
|
||||
`BulkPushRuleEvaluator`).
|
||||
|
||||
For the latter we could simply count the number of rows in `event_push_actions`
|
||||
table for a given room/user, but in practice this is *very* heavyweight when
|
||||
there were a large number of notifications (due to e.g. the user never reading a
|
||||
room). Plus, keeping all push actions indefinitely uses a lot of disk space.
|
||||
|
||||
To fix these issues, we add a new table `event_push_summary` that tracks
|
||||
per-user per-room counts of all notifications that happened before a stream
|
||||
ordering S. Thus, to get the notification count for a user / room we can simply
|
||||
query a single row in `event_push_summary` and count the number of rows in
|
||||
`event_push_actions` with a stream ordering larger than S (and as long as S is
|
||||
"recent", the number of rows needing to be scanned will be small).
|
||||
|
||||
The `event_push_summary` table is updated via a background job that periodically
|
||||
chooses a new stream ordering S' (usually the latest stream ordering), counts
|
||||
all notifications in `event_push_actions` between the existing S and S', and
|
||||
adds them to the existing counts in `event_push_summary`.
|
||||
|
||||
This allows us to delete old rows from `event_push_actions` once those rows have
|
||||
been counted and added to `event_push_summary` (we call this process
|
||||
"rotation").
|
||||
|
||||
|
||||
We need to handle when a user sends a read receipt to the room. Again this is
|
||||
done as a background process. For each receipt we clear the row in
|
||||
`event_push_summary` and count the number of notifications in
|
||||
`event_push_actions` that happened after the receipt but before S, and insert
|
||||
that count into `event_push_summary` (If the receipt happened *after* S then we
|
||||
simply clear the `event_push_summary`.)
|
||||
|
||||
Note that its possible that if the read receipt is for an old event the relevant
|
||||
`event_push_actions` rows will have been rotated and we get the wrong count
|
||||
(it'll be too low). We accept this as a rare edge case that is unlikely to
|
||||
impact the user much (since the vast majority of read receipts will be for the
|
||||
latest event).
|
||||
|
||||
The last complication is to handle the race where we request the notifications
|
||||
counts after a user sends a read receipt into the room, but *before* the
|
||||
background update handles the receipt (without any special handling the counts
|
||||
would be outdated). We fix this by including in `event_push_summary` the read
|
||||
receipt we used when updating `event_push_summary`, and every time we query the
|
||||
table we check if that matches the most recent read receipt in the room. If yes,
|
||||
continue as above, if not we simply query the `event_push_actions` table
|
||||
directly.
|
||||
|
||||
Since read receipts are almost always for recent events, scanning the
|
||||
`event_push_actions` table in this case is unlikely to be a problem. Even if it
|
||||
is a problem, it is temporary until the background job handles the new read
|
||||
receipt.
|
||||
"""
|
||||
|
||||
import logging
|
||||
from typing import TYPE_CHECKING, Dict, List, Optional, Tuple, Union, cast
|
||||
|
||||
@@ -19,7 +80,7 @@ import attr
|
||||
|
||||
from synapse.api.constants import ReceiptTypes
|
||||
from synapse.metrics.background_process_metrics import wrap_as_background_process
|
||||
from synapse.storage._base import SQLBaseStore, db_to_json
|
||||
from synapse.storage._base import SQLBaseStore, db_to_json, make_in_list_sql_clause
|
||||
from synapse.storage.database import (
|
||||
DatabasePool,
|
||||
LoggingDatabaseConnection,
|
||||
@@ -166,7 +227,7 @@ class EventPushActionsWorkerStore(ReceiptsWorkerStore, StreamWorkerStore, SQLBas
|
||||
user_id: str,
|
||||
) -> NotifCounts:
|
||||
"""Get the notification count, the highlight count and the unread message count
|
||||
for a given user in a given room after the given read receipt.
|
||||
for a given user in a given room after their latest read receipt.
|
||||
|
||||
Note that this function assumes the user to be a current member of the room,
|
||||
since it's either called by the sync handler to handle joined room entries, or by
|
||||
@@ -177,9 +238,8 @@ class EventPushActionsWorkerStore(ReceiptsWorkerStore, StreamWorkerStore, SQLBas
|
||||
user_id: The user to retrieve the counts for.
|
||||
|
||||
Returns
|
||||
A dict containing the counts mentioned earlier in this docstring,
|
||||
respectively under the keys "notify_count", "highlight_count" and
|
||||
"unread_count".
|
||||
A NotifCounts object containing the notification count, the highlight count
|
||||
and the unread message count.
|
||||
"""
|
||||
return await self.db_pool.runInteraction(
|
||||
"get_unread_event_push_actions_by_room",
|
||||
@@ -194,20 +254,23 @@ class EventPushActionsWorkerStore(ReceiptsWorkerStore, StreamWorkerStore, SQLBas
|
||||
room_id: str,
|
||||
user_id: str,
|
||||
) -> NotifCounts:
|
||||
# Get the stream ordering of the user's latest receipt in the room.
|
||||
result = self.get_last_receipt_for_user_txn(
|
||||
txn,
|
||||
user_id,
|
||||
room_id,
|
||||
receipt_types=(ReceiptTypes.READ, ReceiptTypes.READ_PRIVATE),
|
||||
receipt_types=(
|
||||
ReceiptTypes.READ,
|
||||
ReceiptTypes.READ_PRIVATE,
|
||||
ReceiptTypes.UNSTABLE_READ_PRIVATE,
|
||||
),
|
||||
)
|
||||
|
||||
stream_ordering = None
|
||||
if result:
|
||||
_, stream_ordering = result
|
||||
|
||||
if stream_ordering is None:
|
||||
# Either last_read_event_id is None, or it's an event we don't have (e.g.
|
||||
# because it's been purged), in which case retrieve the stream ordering for
|
||||
else:
|
||||
# If the user has no receipts in the room, retrieve the stream ordering for
|
||||
# the latest membership event from this user in this room (which we assume is
|
||||
# a join).
|
||||
event_id = self.db_pool.simple_select_one_onecol_txn(
|
||||
@@ -224,10 +287,26 @@ class EventPushActionsWorkerStore(ReceiptsWorkerStore, StreamWorkerStore, SQLBas
|
||||
)
|
||||
|
||||
def _get_unread_counts_by_pos_txn(
|
||||
self, txn: LoggingTransaction, room_id: str, user_id: str, stream_ordering: int
|
||||
self,
|
||||
txn: LoggingTransaction,
|
||||
room_id: str,
|
||||
user_id: str,
|
||||
receipt_stream_ordering: int,
|
||||
) -> NotifCounts:
|
||||
"""Get the number of unread messages for a user/room that have happened
|
||||
since the given stream ordering.
|
||||
|
||||
Args:
|
||||
txn: The database transaction.
|
||||
room_id: The room ID to get unread counts for.
|
||||
user_id: The user ID to get unread counts for.
|
||||
receipt_stream_ordering: The stream ordering of the user's latest
|
||||
receipt in the room. If there are no receipts, the stream ordering
|
||||
of the user's join event.
|
||||
|
||||
Returns
|
||||
A NotifCounts object containing the notification count, the highlight count
|
||||
and the unread message count.
|
||||
"""
|
||||
|
||||
counts = NotifCounts()
|
||||
@@ -255,7 +334,7 @@ class EventPushActionsWorkerStore(ReceiptsWorkerStore, StreamWorkerStore, SQLBas
|
||||
OR last_receipt_stream_ordering = ?
|
||||
)
|
||||
""",
|
||||
(room_id, user_id, stream_ordering, stream_ordering),
|
||||
(room_id, user_id, receipt_stream_ordering, receipt_stream_ordering),
|
||||
)
|
||||
row = txn.fetchone()
|
||||
|
||||
@@ -265,7 +344,7 @@ class EventPushActionsWorkerStore(ReceiptsWorkerStore, StreamWorkerStore, SQLBas
|
||||
counts.notify_count += row[1]
|
||||
counts.unread_count += row[2]
|
||||
|
||||
# Next we need to count highlights, which aren't summarized
|
||||
# Next we need to count highlights, which aren't summarised
|
||||
sql = """
|
||||
SELECT COUNT(*) FROM event_push_actions
|
||||
WHERE user_id = ?
|
||||
@@ -273,17 +352,20 @@ class EventPushActionsWorkerStore(ReceiptsWorkerStore, StreamWorkerStore, SQLBas
|
||||
AND stream_ordering > ?
|
||||
AND highlight = 1
|
||||
"""
|
||||
txn.execute(sql, (user_id, room_id, stream_ordering))
|
||||
txn.execute(sql, (user_id, room_id, receipt_stream_ordering))
|
||||
row = txn.fetchone()
|
||||
if row:
|
||||
counts.highlight_count += row[0]
|
||||
|
||||
# Finally we need to count push actions that aren't included in the
|
||||
# summary returned above, e.g. recent events that haven't been
|
||||
# summarized yet, or the summary is empty due to a recent read receipt.
|
||||
stream_ordering = max(stream_ordering, summary_stream_ordering)
|
||||
# summary returned above. This might be due to recent events that haven't
|
||||
# been summarised yet or the summary is out of date due to a recent read
|
||||
# receipt.
|
||||
start_unread_stream_ordering = max(
|
||||
receipt_stream_ordering, summary_stream_ordering
|
||||
)
|
||||
notify_count, unread_count = self._get_notif_unread_count_for_user_room(
|
||||
txn, room_id, user_id, stream_ordering
|
||||
txn, room_id, user_id, start_unread_stream_ordering
|
||||
)
|
||||
|
||||
counts.notify_count += notify_count
|
||||
@@ -304,6 +386,17 @@ class EventPushActionsWorkerStore(ReceiptsWorkerStore, StreamWorkerStore, SQLBas
|
||||
|
||||
Does not consult `event_push_summary` table, which may include push
|
||||
actions that have been deleted from `event_push_actions` table.
|
||||
|
||||
Args:
|
||||
txn: The database transaction.
|
||||
room_id: The room ID to get unread counts for.
|
||||
user_id: The user ID to get unread counts for.
|
||||
stream_ordering: The (exclusive) minimum stream ordering to consider.
|
||||
max_stream_ordering: The (inclusive) maximum stream ordering to consider.
|
||||
If this is not given, then no maximum is applied.
|
||||
|
||||
Return:
|
||||
A tuple of the notif count and unread count in the given range.
|
||||
"""
|
||||
|
||||
# If there have been no events in the room since the stream ordering,
|
||||
@@ -376,6 +469,7 @@ class EventPushActionsWorkerStore(ReceiptsWorkerStore, StreamWorkerStore, SQLBas
|
||||
The list will be ordered by ascending stream_ordering.
|
||||
The list will have between 0~limit entries.
|
||||
"""
|
||||
|
||||
# find rooms that have a read receipt in them and return the next
|
||||
# push actions
|
||||
def get_after_receipt(
|
||||
@@ -383,28 +477,41 @@ class EventPushActionsWorkerStore(ReceiptsWorkerStore, StreamWorkerStore, SQLBas
|
||||
) -> List[Tuple[str, str, int, str, bool]]:
|
||||
# find rooms that have a read receipt in them and return the next
|
||||
# push actions
|
||||
sql = (
|
||||
"SELECT ep.event_id, ep.room_id, ep.stream_ordering, ep.actions,"
|
||||
" ep.highlight "
|
||||
" FROM ("
|
||||
" SELECT room_id,"
|
||||
" MAX(stream_ordering) as stream_ordering"
|
||||
" FROM events"
|
||||
" INNER JOIN receipts_linearized USING (room_id, event_id)"
|
||||
" WHERE receipt_type = 'm.read' AND user_id = ?"
|
||||
" GROUP BY room_id"
|
||||
") AS rl,"
|
||||
" event_push_actions AS ep"
|
||||
" WHERE"
|
||||
" ep.room_id = rl.room_id"
|
||||
" AND ep.stream_ordering > rl.stream_ordering"
|
||||
" AND ep.user_id = ?"
|
||||
" AND ep.stream_ordering > ?"
|
||||
" AND ep.stream_ordering <= ?"
|
||||
" AND ep.notif = 1"
|
||||
" ORDER BY ep.stream_ordering ASC LIMIT ?"
|
||||
|
||||
receipt_types_clause, args = make_in_list_sql_clause(
|
||||
self.database_engine,
|
||||
"receipt_type",
|
||||
(
|
||||
ReceiptTypes.READ,
|
||||
ReceiptTypes.READ_PRIVATE,
|
||||
ReceiptTypes.UNSTABLE_READ_PRIVATE,
|
||||
),
|
||||
)
|
||||
|
||||
sql = f"""
|
||||
SELECT ep.event_id, ep.room_id, ep.stream_ordering, ep.actions,
|
||||
ep.highlight
|
||||
FROM (
|
||||
SELECT room_id,
|
||||
MAX(stream_ordering) as stream_ordering
|
||||
FROM events
|
||||
INNER JOIN receipts_linearized USING (room_id, event_id)
|
||||
WHERE {receipt_types_clause} AND user_id = ?
|
||||
GROUP BY room_id
|
||||
) AS rl,
|
||||
event_push_actions AS ep
|
||||
WHERE
|
||||
ep.room_id = rl.room_id
|
||||
AND ep.stream_ordering > rl.stream_ordering
|
||||
AND ep.user_id = ?
|
||||
AND ep.stream_ordering > ?
|
||||
AND ep.stream_ordering <= ?
|
||||
AND ep.notif = 1
|
||||
ORDER BY ep.stream_ordering ASC LIMIT ?
|
||||
"""
|
||||
args.extend(
|
||||
(user_id, user_id, min_stream_ordering, max_stream_ordering, limit)
|
||||
)
|
||||
args = [user_id, user_id, min_stream_ordering, max_stream_ordering, limit]
|
||||
txn.execute(sql, args)
|
||||
return cast(List[Tuple[str, str, int, str, bool]], txn.fetchall())
|
||||
|
||||
@@ -418,24 +525,36 @@ class EventPushActionsWorkerStore(ReceiptsWorkerStore, StreamWorkerStore, SQLBas
|
||||
def get_no_receipt(
|
||||
txn: LoggingTransaction,
|
||||
) -> List[Tuple[str, str, int, str, bool]]:
|
||||
sql = (
|
||||
"SELECT ep.event_id, ep.room_id, ep.stream_ordering, ep.actions,"
|
||||
" ep.highlight "
|
||||
" FROM event_push_actions AS ep"
|
||||
" INNER JOIN events AS e USING (room_id, event_id)"
|
||||
" WHERE"
|
||||
" ep.room_id NOT IN ("
|
||||
" SELECT room_id FROM receipts_linearized"
|
||||
" WHERE receipt_type = 'm.read' AND user_id = ?"
|
||||
" GROUP BY room_id"
|
||||
" )"
|
||||
" AND ep.user_id = ?"
|
||||
" AND ep.stream_ordering > ?"
|
||||
" AND ep.stream_ordering <= ?"
|
||||
" AND ep.notif = 1"
|
||||
" ORDER BY ep.stream_ordering ASC LIMIT ?"
|
||||
receipt_types_clause, args = make_in_list_sql_clause(
|
||||
self.database_engine,
|
||||
"receipt_type",
|
||||
(
|
||||
ReceiptTypes.READ,
|
||||
ReceiptTypes.READ_PRIVATE,
|
||||
ReceiptTypes.UNSTABLE_READ_PRIVATE,
|
||||
),
|
||||
)
|
||||
|
||||
sql = f"""
|
||||
SELECT ep.event_id, ep.room_id, ep.stream_ordering, ep.actions,
|
||||
ep.highlight
|
||||
FROM event_push_actions AS ep
|
||||
INNER JOIN events AS e USING (room_id, event_id)
|
||||
WHERE
|
||||
ep.room_id NOT IN (
|
||||
SELECT room_id FROM receipts_linearized
|
||||
WHERE {receipt_types_clause} AND user_id = ?
|
||||
GROUP BY room_id
|
||||
)
|
||||
AND ep.user_id = ?
|
||||
AND ep.stream_ordering > ?
|
||||
AND ep.stream_ordering <= ?
|
||||
AND ep.notif = 1
|
||||
ORDER BY ep.stream_ordering ASC LIMIT ?
|
||||
"""
|
||||
args.extend(
|
||||
(user_id, user_id, min_stream_ordering, max_stream_ordering, limit)
|
||||
)
|
||||
args = [user_id, user_id, min_stream_ordering, max_stream_ordering, limit]
|
||||
txn.execute(sql, args)
|
||||
return cast(List[Tuple[str, str, int, str, bool]], txn.fetchall())
|
||||
|
||||
@@ -485,34 +604,47 @@ class EventPushActionsWorkerStore(ReceiptsWorkerStore, StreamWorkerStore, SQLBas
|
||||
The list will be ordered by descending received_ts.
|
||||
The list will have between 0~limit entries.
|
||||
"""
|
||||
|
||||
# find rooms that have a read receipt in them and return the most recent
|
||||
# push actions
|
||||
def get_after_receipt(
|
||||
txn: LoggingTransaction,
|
||||
) -> List[Tuple[str, str, int, str, bool, int]]:
|
||||
sql = (
|
||||
"SELECT ep.event_id, ep.room_id, ep.stream_ordering, ep.actions,"
|
||||
" ep.highlight, e.received_ts"
|
||||
" FROM ("
|
||||
" SELECT room_id,"
|
||||
" MAX(stream_ordering) as stream_ordering"
|
||||
" FROM events"
|
||||
" INNER JOIN receipts_linearized USING (room_id, event_id)"
|
||||
" WHERE receipt_type = 'm.read' AND user_id = ?"
|
||||
" GROUP BY room_id"
|
||||
") AS rl,"
|
||||
" event_push_actions AS ep"
|
||||
" INNER JOIN events AS e USING (room_id, event_id)"
|
||||
" WHERE"
|
||||
" ep.room_id = rl.room_id"
|
||||
" AND ep.stream_ordering > rl.stream_ordering"
|
||||
" AND ep.user_id = ?"
|
||||
" AND ep.stream_ordering > ?"
|
||||
" AND ep.stream_ordering <= ?"
|
||||
" AND ep.notif = 1"
|
||||
" ORDER BY ep.stream_ordering DESC LIMIT ?"
|
||||
receipt_types_clause, args = make_in_list_sql_clause(
|
||||
self.database_engine,
|
||||
"receipt_type",
|
||||
(
|
||||
ReceiptTypes.READ,
|
||||
ReceiptTypes.READ_PRIVATE,
|
||||
ReceiptTypes.UNSTABLE_READ_PRIVATE,
|
||||
),
|
||||
)
|
||||
|
||||
sql = f"""
|
||||
SELECT ep.event_id, ep.room_id, ep.stream_ordering, ep.actions,
|
||||
ep.highlight, e.received_ts
|
||||
FROM (
|
||||
SELECT room_id,
|
||||
MAX(stream_ordering) as stream_ordering
|
||||
FROM events
|
||||
INNER JOIN receipts_linearized USING (room_id, event_id)
|
||||
WHERE {receipt_types_clause} AND user_id = ?
|
||||
GROUP BY room_id
|
||||
) AS rl,
|
||||
event_push_actions AS ep
|
||||
INNER JOIN events AS e USING (room_id, event_id)
|
||||
WHERE
|
||||
ep.room_id = rl.room_id
|
||||
AND ep.stream_ordering > rl.stream_ordering
|
||||
AND ep.user_id = ?
|
||||
AND ep.stream_ordering > ?
|
||||
AND ep.stream_ordering <= ?
|
||||
AND ep.notif = 1
|
||||
ORDER BY ep.stream_ordering DESC LIMIT ?
|
||||
"""
|
||||
args.extend(
|
||||
(user_id, user_id, min_stream_ordering, max_stream_ordering, limit)
|
||||
)
|
||||
args = [user_id, user_id, min_stream_ordering, max_stream_ordering, limit]
|
||||
txn.execute(sql, args)
|
||||
return cast(List[Tuple[str, str, int, str, bool, int]], txn.fetchall())
|
||||
|
||||
@@ -526,24 +658,36 @@ class EventPushActionsWorkerStore(ReceiptsWorkerStore, StreamWorkerStore, SQLBas
|
||||
def get_no_receipt(
|
||||
txn: LoggingTransaction,
|
||||
) -> List[Tuple[str, str, int, str, bool, int]]:
|
||||
sql = (
|
||||
"SELECT ep.event_id, ep.room_id, ep.stream_ordering, ep.actions,"
|
||||
" ep.highlight, e.received_ts"
|
||||
" FROM event_push_actions AS ep"
|
||||
" INNER JOIN events AS e USING (room_id, event_id)"
|
||||
" WHERE"
|
||||
" ep.room_id NOT IN ("
|
||||
" SELECT room_id FROM receipts_linearized"
|
||||
" WHERE receipt_type = 'm.read' AND user_id = ?"
|
||||
" GROUP BY room_id"
|
||||
" )"
|
||||
" AND ep.user_id = ?"
|
||||
" AND ep.stream_ordering > ?"
|
||||
" AND ep.stream_ordering <= ?"
|
||||
" AND ep.notif = 1"
|
||||
" ORDER BY ep.stream_ordering DESC LIMIT ?"
|
||||
receipt_types_clause, args = make_in_list_sql_clause(
|
||||
self.database_engine,
|
||||
"receipt_type",
|
||||
(
|
||||
ReceiptTypes.READ,
|
||||
ReceiptTypes.READ_PRIVATE,
|
||||
ReceiptTypes.UNSTABLE_READ_PRIVATE,
|
||||
),
|
||||
)
|
||||
|
||||
sql = f"""
|
||||
SELECT ep.event_id, ep.room_id, ep.stream_ordering, ep.actions,
|
||||
ep.highlight, e.received_ts
|
||||
FROM event_push_actions AS ep
|
||||
INNER JOIN events AS e USING (room_id, event_id)
|
||||
WHERE
|
||||
ep.room_id NOT IN (
|
||||
SELECT room_id FROM receipts_linearized
|
||||
WHERE {receipt_types_clause} AND user_id = ?
|
||||
GROUP BY room_id
|
||||
)
|
||||
AND ep.user_id = ?
|
||||
AND ep.stream_ordering > ?
|
||||
AND ep.stream_ordering <= ?
|
||||
AND ep.notif = 1
|
||||
ORDER BY ep.stream_ordering DESC LIMIT ?
|
||||
"""
|
||||
args.extend(
|
||||
(user_id, user_id, min_stream_ordering, max_stream_ordering, limit)
|
||||
)
|
||||
args = [user_id, user_id, min_stream_ordering, max_stream_ordering, limit]
|
||||
txn.execute(sql, args)
|
||||
return cast(List[Tuple[str, str, int, str, bool, int]], txn.fetchall())
|
||||
|
||||
@@ -769,12 +913,12 @@ class EventPushActionsWorkerStore(ReceiptsWorkerStore, StreamWorkerStore, SQLBas
|
||||
# [10, <none>, 20], we should treat this as being equivalent to
|
||||
# [10, 10, 20].
|
||||
#
|
||||
sql = (
|
||||
"SELECT received_ts FROM events"
|
||||
" WHERE stream_ordering <= ?"
|
||||
" ORDER BY stream_ordering DESC"
|
||||
" LIMIT 1"
|
||||
)
|
||||
sql = """
|
||||
SELECT received_ts FROM events
|
||||
WHERE stream_ordering <= ?
|
||||
ORDER BY stream_ordering DESC
|
||||
LIMIT 1
|
||||
"""
|
||||
|
||||
while range_end - range_start > 0:
|
||||
middle = (range_end + range_start) // 2
|
||||
@@ -802,14 +946,14 @@ class EventPushActionsWorkerStore(ReceiptsWorkerStore, StreamWorkerStore, SQLBas
|
||||
self, stream_ordering: int
|
||||
) -> Optional[int]:
|
||||
def f(txn: LoggingTransaction) -> Optional[Tuple[int]]:
|
||||
sql = (
|
||||
"SELECT e.received_ts"
|
||||
" FROM event_push_actions AS ep"
|
||||
" JOIN events e ON ep.room_id = e.room_id AND ep.event_id = e.event_id"
|
||||
" WHERE ep.stream_ordering > ? AND notif = 1"
|
||||
" ORDER BY ep.stream_ordering ASC"
|
||||
" LIMIT 1"
|
||||
)
|
||||
sql = """
|
||||
SELECT e.received_ts
|
||||
FROM event_push_actions AS ep
|
||||
JOIN events e ON ep.room_id = e.room_id AND ep.event_id = e.event_id
|
||||
WHERE ep.stream_ordering > ? AND notif = 1
|
||||
ORDER BY ep.stream_ordering ASC
|
||||
LIMIT 1
|
||||
"""
|
||||
txn.execute(sql, (stream_ordering,))
|
||||
return cast(Optional[Tuple[int]], txn.fetchone())
|
||||
|
||||
@@ -858,10 +1002,13 @@ class EventPushActionsWorkerStore(ReceiptsWorkerStore, StreamWorkerStore, SQLBas
|
||||
Any push actions which predate the user's most recent read receipt are
|
||||
now redundant, so we can remove them from `event_push_actions` and
|
||||
update `event_push_summary`.
|
||||
|
||||
Returns true if all new receipts have been processed.
|
||||
"""
|
||||
|
||||
limit = 100
|
||||
|
||||
# The (inclusive) receipt stream ID that was previously processed..
|
||||
min_receipts_stream_id = self.db_pool.simple_select_one_onecol_txn(
|
||||
txn,
|
||||
table="event_push_summary_last_receipt_stream_id",
|
||||
@@ -871,6 +1018,14 @@ class EventPushActionsWorkerStore(ReceiptsWorkerStore, StreamWorkerStore, SQLBas
|
||||
|
||||
max_receipts_stream_id = self._receipts_id_gen.get_current_token()
|
||||
|
||||
# The (inclusive) event stream ordering that was previously summarised.
|
||||
old_rotate_stream_ordering = self.db_pool.simple_select_one_onecol_txn(
|
||||
txn,
|
||||
table="event_push_summary_stream_ordering",
|
||||
keyvalues={},
|
||||
retcol="stream_ordering",
|
||||
)
|
||||
|
||||
sql = """
|
||||
SELECT r.stream_id, r.room_id, r.user_id, e.stream_ordering
|
||||
FROM receipts_linearized AS r
|
||||
@@ -895,13 +1050,6 @@ class EventPushActionsWorkerStore(ReceiptsWorkerStore, StreamWorkerStore, SQLBas
|
||||
)
|
||||
rows = txn.fetchall()
|
||||
|
||||
old_rotate_stream_ordering = self.db_pool.simple_select_one_onecol_txn(
|
||||
txn,
|
||||
table="event_push_summary_stream_ordering",
|
||||
keyvalues={},
|
||||
retcol="stream_ordering",
|
||||
)
|
||||
|
||||
# For each new read receipt we delete push actions from before it and
|
||||
# recalculate the summary.
|
||||
for _, room_id, user_id, stream_ordering in rows:
|
||||
@@ -920,10 +1068,13 @@ class EventPushActionsWorkerStore(ReceiptsWorkerStore, StreamWorkerStore, SQLBas
|
||||
(room_id, user_id, stream_ordering),
|
||||
)
|
||||
|
||||
# Fetch the notification counts between the stream ordering of the
|
||||
# latest receipt and what was previously summarised.
|
||||
notif_count, unread_count = self._get_notif_unread_count_for_user_room(
|
||||
txn, room_id, user_id, stream_ordering, old_rotate_stream_ordering
|
||||
)
|
||||
|
||||
# Replace the previous summary with the new counts.
|
||||
self.db_pool.simple_upsert_txn(
|
||||
txn,
|
||||
table="event_push_summary",
|
||||
@@ -956,10 +1107,12 @@ class EventPushActionsWorkerStore(ReceiptsWorkerStore, StreamWorkerStore, SQLBas
|
||||
return len(rows) < limit
|
||||
|
||||
def _rotate_notifs_txn(self, txn: LoggingTransaction) -> bool:
|
||||
"""Archives older notifications into event_push_summary. Returns whether
|
||||
the archiving process has caught up or not.
|
||||
"""Archives older notifications (from event_push_actions) into event_push_summary.
|
||||
|
||||
Returns whether the archiving process has caught up or not.
|
||||
"""
|
||||
|
||||
# The (inclusive) event stream ordering that was previously summarised.
|
||||
old_rotate_stream_ordering = self.db_pool.simple_select_one_onecol_txn(
|
||||
txn,
|
||||
table="event_push_summary_stream_ordering",
|
||||
@@ -974,7 +1127,7 @@ class EventPushActionsWorkerStore(ReceiptsWorkerStore, StreamWorkerStore, SQLBas
|
||||
SELECT stream_ordering FROM event_push_actions
|
||||
WHERE stream_ordering > ?
|
||||
ORDER BY stream_ordering ASC LIMIT 1 OFFSET ?
|
||||
""",
|
||||
""",
|
||||
(old_rotate_stream_ordering, self._rotate_count),
|
||||
)
|
||||
stream_row = txn.fetchone()
|
||||
@@ -993,19 +1146,29 @@ class EventPushActionsWorkerStore(ReceiptsWorkerStore, StreamWorkerStore, SQLBas
|
||||
|
||||
logger.info("Rotating notifications up to: %s", rotate_to_stream_ordering)
|
||||
|
||||
self._rotate_notifs_before_txn(txn, rotate_to_stream_ordering)
|
||||
self._rotate_notifs_before_txn(
|
||||
txn, old_rotate_stream_ordering, rotate_to_stream_ordering
|
||||
)
|
||||
|
||||
return caught_up
|
||||
|
||||
def _rotate_notifs_before_txn(
|
||||
self, txn: LoggingTransaction, rotate_to_stream_ordering: int
|
||||
self,
|
||||
txn: LoggingTransaction,
|
||||
old_rotate_stream_ordering: int,
|
||||
rotate_to_stream_ordering: int,
|
||||
) -> None:
|
||||
old_rotate_stream_ordering = self.db_pool.simple_select_one_onecol_txn(
|
||||
txn,
|
||||
table="event_push_summary_stream_ordering",
|
||||
keyvalues={},
|
||||
retcol="stream_ordering",
|
||||
)
|
||||
"""Archives older notifications (from event_push_actions) into event_push_summary.
|
||||
|
||||
Any event_push_actions between old_rotate_stream_ordering (exclusive) and
|
||||
rotate_to_stream_ordering (inclusive) will be added to the event_push_summary
|
||||
table.
|
||||
|
||||
Args:
|
||||
txn: The database transaction.
|
||||
old_rotate_stream_ordering: The previous maximum event stream ordering.
|
||||
rotate_to_stream_ordering: The new maximum event stream ordering to summarise.
|
||||
"""
|
||||
|
||||
# Calculate the new counts that should be upserted into event_push_summary
|
||||
sql = """
|
||||
@@ -1090,12 +1253,10 @@ class EventPushActionsWorkerStore(ReceiptsWorkerStore, StreamWorkerStore, SQLBas
|
||||
(rotate_to_stream_ordering,),
|
||||
)
|
||||
|
||||
async def _remove_old_push_actions_that_have_rotated(
|
||||
self,
|
||||
) -> None:
|
||||
"""Clear out old push actions that have been summarized."""
|
||||
async def _remove_old_push_actions_that_have_rotated(self) -> None:
|
||||
"""Clear out old push actions that have been summarised."""
|
||||
|
||||
# We want to clear out anything that older than a day that *has* already
|
||||
# We want to clear out anything that is older than a day that *has* already
|
||||
# been rotated.
|
||||
rotated_upto_stream_ordering = await self.db_pool.simple_select_one_onecol(
|
||||
table="event_push_summary_stream_ordering",
|
||||
@@ -1119,7 +1280,7 @@ class EventPushActionsWorkerStore(ReceiptsWorkerStore, StreamWorkerStore, SQLBas
|
||||
SELECT stream_ordering FROM event_push_actions
|
||||
WHERE stream_ordering <= ? AND highlight = 0
|
||||
ORDER BY stream_ordering ASC LIMIT 1 OFFSET ?
|
||||
""",
|
||||
""",
|
||||
(
|
||||
max_stream_ordering_to_delete,
|
||||
batch_size,
|
||||
@@ -1215,16 +1376,18 @@ class EventPushActionsStore(EventPushActionsWorkerStore):
|
||||
|
||||
# NB. This assumes event_ids are globally unique since
|
||||
# it makes the query easier to index
|
||||
sql = (
|
||||
"SELECT epa.event_id, epa.room_id,"
|
||||
" epa.stream_ordering, epa.topological_ordering,"
|
||||
" epa.actions, epa.highlight, epa.profile_tag, e.received_ts"
|
||||
" FROM event_push_actions epa, events e"
|
||||
" WHERE epa.event_id = e.event_id"
|
||||
" AND epa.user_id = ? %s"
|
||||
" AND epa.notif = 1"
|
||||
" ORDER BY epa.stream_ordering DESC"
|
||||
" LIMIT ?" % (before_clause,)
|
||||
sql = """
|
||||
SELECT epa.event_id, epa.room_id,
|
||||
epa.stream_ordering, epa.topological_ordering,
|
||||
epa.actions, epa.highlight, epa.profile_tag, e.received_ts
|
||||
FROM event_push_actions epa, events e
|
||||
WHERE epa.event_id = e.event_id
|
||||
AND epa.user_id = ? %s
|
||||
AND epa.notif = 1
|
||||
ORDER BY epa.stream_ordering DESC
|
||||
LIMIT ?
|
||||
""" % (
|
||||
before_clause,
|
||||
)
|
||||
txn.execute(sql, args)
|
||||
return cast(
|
||||
|
||||
@@ -1490,7 +1490,7 @@ class PersistEventsStore:
|
||||
event.sender,
|
||||
"url" in event.content and isinstance(event.content["url"], str),
|
||||
event.get_state_key(),
|
||||
context.rejected or None,
|
||||
context.rejected,
|
||||
)
|
||||
for event, context in events_and_contexts
|
||||
),
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user