Compare commits
10 Commits
release-v1
...
rei/synwor
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
00b5bba66e | ||
|
|
01747a28ca | ||
|
|
c347978164 | ||
|
|
415ba59e8b | ||
|
|
2b0ebf1b4d | ||
|
|
0b9f7b123e | ||
|
|
4332493df3 | ||
|
|
b8c6fd979a | ||
|
|
c14dcea4b6 | ||
|
|
22a7b762bc |
@@ -7,4 +7,3 @@ root = true
|
||||
[*.py]
|
||||
indent_style = space
|
||||
indent_size = 4
|
||||
max_line_length = 88
|
||||
|
||||
327
.github/workflows/tests.yml
vendored
327
.github/workflows/tests.yml
vendored
@@ -10,324 +10,15 @@ concurrency:
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
check-sampleconfig:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/setup-python@v2
|
||||
- run: pip install .
|
||||
- run: scripts-dev/generate_sample_config.sh --check
|
||||
- run: scripts-dev/config-lint.sh
|
||||
|
||||
check-schema-delta:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/setup-python@v2
|
||||
- run: "pip install 'click==8.1.1' 'GitPython>=3.1.20'"
|
||||
- run: scripts-dev/check_schema_delta.py --force-colors
|
||||
|
||||
lint:
|
||||
uses: "matrix-org/backend-meta/.github/workflows/python-poetry-ci.yml@v1"
|
||||
with:
|
||||
typechecking-extras: "all"
|
||||
|
||||
lint-crlf:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: Check line endings
|
||||
run: scripts-dev/check_line_terminators.sh
|
||||
|
||||
lint-newsfile:
|
||||
if: ${{ github.base_ref == 'develop' || contains(github.base_ref, 'release-') }}
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
with:
|
||||
ref: ${{ github.event.pull_request.head.sha }}
|
||||
fetch-depth: 0
|
||||
- uses: actions/setup-python@v2
|
||||
- run: "pip install 'towncrier>=18.6.0rc1'"
|
||||
- run: scripts-dev/check-newsfragment.sh
|
||||
env:
|
||||
PULL_REQUEST_NUMBER: ${{ github.event.number }}
|
||||
|
||||
# Dummy step to gate other tests on without repeating the whole list
|
||||
linting-done:
|
||||
if: ${{ !cancelled() }} # Run this even if prior jobs were skipped
|
||||
needs: [lint, lint-crlf, lint-newsfile, check-sampleconfig, check-schema-delta]
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- run: "true"
|
||||
|
||||
trial:
|
||||
if: ${{ !cancelled() && !failure() }} # Allow previous steps to be skipped, but not fail
|
||||
needs: linting-done
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
python-version: ["3.7", "3.8", "3.9", "3.10"]
|
||||
database: ["sqlite"]
|
||||
extras: ["all"]
|
||||
include:
|
||||
# Newest Python without optional deps
|
||||
- python-version: "3.10"
|
||||
extras: ""
|
||||
|
||||
# Oldest Python with PostgreSQL
|
||||
- python-version: "3.7"
|
||||
database: "postgres"
|
||||
postgres-version: "10"
|
||||
extras: "all"
|
||||
|
||||
# Newest Python with newest PostgreSQL
|
||||
- python-version: "3.10"
|
||||
database: "postgres"
|
||||
postgres-version: "14"
|
||||
extras: "all"
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- run: sudo apt-get -qq install xmlsec1
|
||||
- name: Set up PostgreSQL ${{ matrix.postgres-version }}
|
||||
if: ${{ matrix.postgres-version }}
|
||||
run: |
|
||||
docker run -d -p 5432:5432 \
|
||||
-e POSTGRES_PASSWORD=postgres \
|
||||
-e POSTGRES_INITDB_ARGS="--lc-collate C --lc-ctype C --encoding UTF8" \
|
||||
postgres:${{ matrix.postgres-version }}
|
||||
- uses: matrix-org/setup-python-poetry@v1
|
||||
with:
|
||||
python-version: ${{ matrix.python-version }}
|
||||
extras: ${{ matrix.extras }}
|
||||
- name: Await PostgreSQL
|
||||
if: ${{ matrix.postgres-version }}
|
||||
timeout-minutes: 2
|
||||
run: until pg_isready -h localhost; do sleep 1; done
|
||||
- run: poetry run trial --jobs=2 tests
|
||||
env:
|
||||
SYNAPSE_POSTGRES: ${{ matrix.database == 'postgres' || '' }}
|
||||
SYNAPSE_POSTGRES_HOST: localhost
|
||||
SYNAPSE_POSTGRES_USER: postgres
|
||||
SYNAPSE_POSTGRES_PASSWORD: postgres
|
||||
- name: Dump logs
|
||||
# Logs are most useful when the command fails, always include them.
|
||||
if: ${{ always() }}
|
||||
# Note: Dumps to workflow logs instead of using actions/upload-artifact
|
||||
# This keeps logs colocated with failing jobs
|
||||
# It also ignores find's exit code; this is a best effort affair
|
||||
run: >-
|
||||
find _trial_temp -name '*.log'
|
||||
-exec echo "::group::{}" \;
|
||||
-exec cat {} \;
|
||||
-exec echo "::endgroup::" \;
|
||||
|| true
|
||||
|
||||
trial-olddeps:
|
||||
# Note: sqlite only; no postgres
|
||||
if: ${{ !cancelled() && !failure() }} # Allow previous steps to be skipped, but not fail
|
||||
needs: linting-done
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: Test with old deps
|
||||
uses: docker://ubuntu:focal # For old python and sqlite
|
||||
# Note: focal seems to be using 3.8, but the oldest is 3.7?
|
||||
# See https://github.com/matrix-org/synapse/issues/12343
|
||||
with:
|
||||
workdir: /github/workspace
|
||||
entrypoint: .ci/scripts/test_old_deps.sh
|
||||
- name: Dump logs
|
||||
# Logs are most useful when the command fails, always include them.
|
||||
if: ${{ always() }}
|
||||
# Note: Dumps to workflow logs instead of using actions/upload-artifact
|
||||
# This keeps logs colocated with failing jobs
|
||||
# It also ignores find's exit code; this is a best effort affair
|
||||
run: >-
|
||||
find _trial_temp -name '*.log'
|
||||
-exec echo "::group::{}" \;
|
||||
-exec cat {} \;
|
||||
-exec echo "::endgroup::" \;
|
||||
|| true
|
||||
|
||||
trial-pypy:
|
||||
# Very slow; only run if the branch name includes 'pypy'
|
||||
# Note: sqlite only; no postgres. Completely untested since poetry move.
|
||||
if: ${{ contains(github.ref, 'pypy') && !failure() && !cancelled() }}
|
||||
needs: linting-done
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
python-version: ["pypy-3.7"]
|
||||
extras: ["all"]
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
# Install libs necessary for PyPy to build binary wheels for dependencies
|
||||
- run: sudo apt-get -qq install xmlsec1 libxml2-dev libxslt-dev
|
||||
- uses: matrix-org/setup-python-poetry@v1
|
||||
with:
|
||||
python-version: ${{ matrix.python-version }}
|
||||
extras: ${{ matrix.extras }}
|
||||
- run: poetry run trial --jobs=2 tests
|
||||
- name: Dump logs
|
||||
# Logs are most useful when the command fails, always include them.
|
||||
if: ${{ always() }}
|
||||
# Note: Dumps to workflow logs instead of using actions/upload-artifact
|
||||
# This keeps logs colocated with failing jobs
|
||||
# It also ignores find's exit code; this is a best effort affair
|
||||
run: >-
|
||||
find _trial_temp -name '*.log'
|
||||
-exec echo "::group::{}" \;
|
||||
-exec cat {} \;
|
||||
-exec echo "::endgroup::" \;
|
||||
|| true
|
||||
|
||||
sytest:
|
||||
if: ${{ !failure() && !cancelled() }}
|
||||
needs: linting-done
|
||||
runs-on: ubuntu-latest
|
||||
container:
|
||||
image: matrixdotorg/sytest-synapse:${{ matrix.sytest-tag }}
|
||||
volumes:
|
||||
- ${{ github.workspace }}:/src
|
||||
env:
|
||||
SYTEST_BRANCH: ${{ github.head_ref }}
|
||||
POSTGRES: ${{ matrix.postgres && 1}}
|
||||
MULTI_POSTGRES: ${{ (matrix.postgres == 'multi-postgres') && 1}}
|
||||
WORKERS: ${{ matrix.workers && 1 }}
|
||||
REDIS: ${{ matrix.redis && 1 }}
|
||||
BLACKLIST: ${{ matrix.workers && 'synapse-blacklist-with-workers' }}
|
||||
TOP: ${{ github.workspace }}
|
||||
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- sytest-tag: focal
|
||||
|
||||
- sytest-tag: focal
|
||||
postgres: postgres
|
||||
|
||||
- sytest-tag: testing
|
||||
postgres: postgres
|
||||
|
||||
- sytest-tag: focal
|
||||
postgres: multi-postgres
|
||||
workers: workers
|
||||
|
||||
- sytest-tag: buster
|
||||
postgres: multi-postgres
|
||||
workers: workers
|
||||
|
||||
- sytest-tag: buster
|
||||
postgres: postgres
|
||||
workers: workers
|
||||
redis: redis
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: Prepare test blacklist
|
||||
run: cat sytest-blacklist .ci/worker-blacklist > synapse-blacklist-with-workers
|
||||
- name: Run SyTest
|
||||
run: /bootstrap.sh synapse
|
||||
working-directory: /src
|
||||
- name: Summarise results.tap
|
||||
if: ${{ always() }}
|
||||
run: /sytest/scripts/tap_to_gha.pl /logs/results.tap
|
||||
- name: Upload SyTest logs
|
||||
uses: actions/upload-artifact@v2
|
||||
if: ${{ always() }}
|
||||
with:
|
||||
name: Sytest Logs - ${{ job.status }} - (${{ join(matrix.*, ', ') }})
|
||||
path: |
|
||||
/logs/results.tap
|
||||
/logs/**/*.log*
|
||||
|
||||
export-data:
|
||||
if: ${{ !failure() && !cancelled() }} # Allow previous steps to be skipped, but not fail
|
||||
needs: [linting-done, portdb]
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
TOP: ${{ github.workspace }}
|
||||
|
||||
services:
|
||||
postgres:
|
||||
image: postgres
|
||||
ports:
|
||||
- 5432:5432
|
||||
env:
|
||||
POSTGRES_PASSWORD: "postgres"
|
||||
POSTGRES_INITDB_ARGS: "--lc-collate C --lc-ctype C --encoding UTF8"
|
||||
options: >-
|
||||
--health-cmd pg_isready
|
||||
--health-interval 10s
|
||||
--health-timeout 5s
|
||||
--health-retries 5
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- run: sudo apt-get -qq install xmlsec1
|
||||
- uses: matrix-org/setup-python-poetry@v1
|
||||
with:
|
||||
python-version: ${{ matrix.python-version }}
|
||||
extras: "postgres"
|
||||
- run: .ci/scripts/test_export_data_command.sh
|
||||
|
||||
portdb:
|
||||
if: ${{ !failure() && !cancelled() }} # Allow previous steps to be skipped, but not fail
|
||||
needs: linting-done
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
TOP: ${{ github.workspace }}
|
||||
strategy:
|
||||
matrix:
|
||||
include:
|
||||
- python-version: "3.7"
|
||||
postgres-version: "10"
|
||||
|
||||
- python-version: "3.10"
|
||||
postgres-version: "14"
|
||||
|
||||
services:
|
||||
postgres:
|
||||
image: postgres:${{ matrix.postgres-version }}
|
||||
ports:
|
||||
- 5432:5432
|
||||
env:
|
||||
POSTGRES_PASSWORD: "postgres"
|
||||
POSTGRES_INITDB_ARGS: "--lc-collate C --lc-ctype C --encoding UTF8"
|
||||
options: >-
|
||||
--health-cmd pg_isready
|
||||
--health-interval 10s
|
||||
--health-timeout 5s
|
||||
--health-retries 5
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- run: sudo apt-get -qq install xmlsec1
|
||||
- uses: matrix-org/setup-python-poetry@v1
|
||||
with:
|
||||
python-version: ${{ matrix.python-version }}
|
||||
extras: "postgres"
|
||||
- run: .ci/scripts/test_synapse_port_db.sh
|
||||
|
||||
complement:
|
||||
if: "${{ !failure() && !cancelled() }}"
|
||||
needs: linting-done
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- arrangement: monolith
|
||||
database: SQLite
|
||||
|
||||
- arrangement: monolith
|
||||
database: Postgres
|
||||
|
||||
- arrangement: workers
|
||||
database: Postgres
|
||||
|
||||
@@ -342,7 +33,14 @@ jobs:
|
||||
|
||||
- run: |
|
||||
set -o pipefail
|
||||
POSTGRES=${{ (matrix.database == 'Postgres') && 1 || '' }} WORKERS=${{ (matrix.arrangement == 'workers') && 1 || '' }} COMPLEMENT_DIR=`pwd`/complement synapse/scripts-dev/complement.sh -json 2>&1 | gotestfmt
|
||||
synapse/scripts-dev/complement.sh --build-only
|
||||
while :; do
|
||||
POSTGRES=${{ (matrix.database == 'Postgres') && 1 || '' }} WORKERS=${{ (matrix.arrangement == 'workers') && 1 || '' }} SYNAPSE_TEST_LOG_LEVEL=DEBUG COMPLEMENT_DIR=`pwd`/complement synapse/scripts-dev/complement.sh -f -run TestSendJoinPartialStateResponse -json 2>&1 | gotestfmt | tee /tmp/xxx
|
||||
if grep ❌ /tmp/xxx; then
|
||||
break
|
||||
fi
|
||||
done
|
||||
|
||||
shell: bash
|
||||
name: Run Complement Tests
|
||||
|
||||
@@ -350,15 +48,6 @@ jobs:
|
||||
tests-done:
|
||||
if: ${{ always() }}
|
||||
needs:
|
||||
- check-sampleconfig
|
||||
- lint
|
||||
- lint-crlf
|
||||
- lint-newsfile
|
||||
- trial
|
||||
- trial-olddeps
|
||||
- sytest
|
||||
- export-data
|
||||
- portdb
|
||||
- complement
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
|
||||
94
CHANGES.md
94
CHANGES.md
@@ -1,97 +1,3 @@
|
||||
Synapse 1.63.1 (2022-07-20)
|
||||
===========================
|
||||
|
||||
Bugfixes
|
||||
--------
|
||||
|
||||
- Fix a bug introduced in Synapse 1.63.0 where push actions were incorrectly calculated for appservice users. This caused performance issues on servers with large numbers of appservices. ([\#13332](https://github.com/matrix-org/synapse/issues/13332))
|
||||
|
||||
|
||||
Synapse 1.63.0 (2022-07-19)
|
||||
===========================
|
||||
|
||||
Improved Documentation
|
||||
----------------------
|
||||
|
||||
- Clarify that homeserver server names are included in the reported data when the `report_stats` config option is enabled. ([\#13321](https://github.com/matrix-org/synapse/issues/13321))
|
||||
|
||||
|
||||
Synapse 1.63.0rc1 (2022-07-12)
|
||||
==============================
|
||||
|
||||
Features
|
||||
--------
|
||||
|
||||
- Add a rate limit for local users sending invites. ([\#13125](https://github.com/matrix-org/synapse/issues/13125))
|
||||
- Implement [MSC3827](https://github.com/matrix-org/matrix-spec-proposals/pull/3827): Filtering of `/publicRooms` by room type. ([\#13031](https://github.com/matrix-org/synapse/issues/13031))
|
||||
- Improve validation logic in the account data REST endpoints. ([\#13148](https://github.com/matrix-org/synapse/issues/13148))
|
||||
|
||||
|
||||
Bugfixes
|
||||
--------
|
||||
|
||||
- Fix a long-standing bug where application services were not able to join remote federated rooms without a profile. ([\#13131](https://github.com/matrix-org/synapse/issues/13131))
|
||||
- Fix a long-standing bug where `_get_state_map_for_room` might raise errors when third party event rules callbacks are present. ([\#13174](https://github.com/matrix-org/synapse/issues/13174))
|
||||
- Fix a long-standing bug where the `synapse_port_db` script could fail to copy rows with negative row ids. ([\#13226](https://github.com/matrix-org/synapse/issues/13226))
|
||||
- Fix a bug introduced in 1.54.0 where appservices would not receive room-less EDUs, like presence, when both [MSC2409](https://github.com/matrix-org/matrix-spec-proposals/pull/2409) and [MSC3202](https://github.com/matrix-org/matrix-spec-proposals/pull/3202) are enabled. ([\#13236](https://github.com/matrix-org/synapse/issues/13236))
|
||||
- Fix a bug introduced in 1.62.0 where rows were not deleted from `event_push_actions` table on large servers. ([\#13194](https://github.com/matrix-org/synapse/issues/13194))
|
||||
- Fix a bug introduced in 1.62.0 where notification counts would get stuck after a highlighted message. ([\#13223](https://github.com/matrix-org/synapse/issues/13223))
|
||||
- Fix exception when using experimental [MSC3030](https://github.com/matrix-org/matrix-spec-proposals/pull/3030) `/timestamp_to_event` endpoint to look for remote federated imported events before room creation. ([\#13197](https://github.com/matrix-org/synapse/issues/13197))
|
||||
- Fix [MSC3202](https://github.com/matrix-org/matrix-spec-proposals/pull/3202)-enabled appservices not receiving to-device messages, preventing messages from being decrypted. ([\#13235](https://github.com/matrix-org/synapse/issues/13235))
|
||||
|
||||
|
||||
Updates to the Docker image
|
||||
---------------------------
|
||||
|
||||
- Bump the version of `lxml` in matrix.org Docker images Debian packages from 4.8.0 to 4.9.1. ([\#13207](https://github.com/matrix-org/synapse/issues/13207))
|
||||
|
||||
|
||||
Improved Documentation
|
||||
----------------------
|
||||
|
||||
- Add an explanation of the `--report-stats` argument to the docs. ([\#13029](https://github.com/matrix-org/synapse/issues/13029))
|
||||
- Add a helpful example bash script to the contrib directory for creating multiple worker configuration files of the same type. Contributed by @villepeh. ([\#13032](https://github.com/matrix-org/synapse/issues/13032))
|
||||
- Add missing links to config options. ([\#13166](https://github.com/matrix-org/synapse/issues/13166))
|
||||
- Add documentation for homeserver usage statistics collection. ([\#13086](https://github.com/matrix-org/synapse/issues/13086))
|
||||
- Add documentation for the existing `databases` option in the homeserver configuration manual. ([\#13212](https://github.com/matrix-org/synapse/issues/13212))
|
||||
- Clean up references to sample configuration and redirect users to the configuration manual instead. ([\#13077](https://github.com/matrix-org/synapse/issues/13077), [\#13139](https://github.com/matrix-org/synapse/issues/13139))
|
||||
- Document how the Synapse team does reviews. ([\#13132](https://github.com/matrix-org/synapse/issues/13132))
|
||||
- Fix wrong section header for `allow_public_rooms_over_federation` in the homeserver config documentation. ([\#13116](https://github.com/matrix-org/synapse/issues/13116))
|
||||
|
||||
|
||||
Deprecations and Removals
|
||||
-------------------------
|
||||
|
||||
- Remove obsolete and for 8 years unused `RoomEventsStoreTestCase`. Contributed by @arkamar. ([\#13200](https://github.com/matrix-org/synapse/issues/13200))
|
||||
|
||||
|
||||
Internal Changes
|
||||
----------------
|
||||
|
||||
- Add type annotations to `synapse.logging`, `tests.server` and `tests.utils`. ([\#13028](https://github.com/matrix-org/synapse/issues/13028), [\#13103](https://github.com/matrix-org/synapse/issues/13103), [\#13159](https://github.com/matrix-org/synapse/issues/13159), [\#13136](https://github.com/matrix-org/synapse/issues/13136))
|
||||
- Enforce type annotations for `tests.test_server`. ([\#13135](https://github.com/matrix-org/synapse/issues/13135))
|
||||
- Support temporary experimental return values for spam checker module callbacks. ([\#13044](https://github.com/matrix-org/synapse/issues/13044))
|
||||
- Add support to `complement.sh` for skipping the docker build. ([\#13143](https://github.com/matrix-org/synapse/issues/13143), [\#13158](https://github.com/matrix-org/synapse/issues/13158))
|
||||
- Add support to `complement.sh` for setting the log level using the `SYNAPSE_TEST_LOG_LEVEL` environment variable. ([\#13152](https://github.com/matrix-org/synapse/issues/13152))
|
||||
- Enable Complement testing in the 'Twisted Trunk' CI runs. ([\#13079](https://github.com/matrix-org/synapse/issues/13079), [\#13157](https://github.com/matrix-org/synapse/issues/13157))
|
||||
- Improve startup times in Complement test runs against workers, particularly in CPU-constrained environments. ([\#13127](https://github.com/matrix-org/synapse/issues/13127))
|
||||
- Update config used by Complement to allow device name lookup over federation. ([\#13167](https://github.com/matrix-org/synapse/issues/13167))
|
||||
- Faster room joins: handle race between persisting an event and un-partial stating a room. ([\#13100](https://github.com/matrix-org/synapse/issues/13100))
|
||||
- Faster room joins: fix race in recalculation of current room state. ([\#13151](https://github.com/matrix-org/synapse/issues/13151))
|
||||
- Faster room joins: skip waiting for full state when processing incoming events over federation. ([\#13144](https://github.com/matrix-org/synapse/issues/13144))
|
||||
- Raise a `DependencyError` on missing dependencies instead of a `ConfigError`. ([\#13113](https://github.com/matrix-org/synapse/issues/13113))
|
||||
- Avoid stripping line breaks from SQL sent to the database. ([\#13129](https://github.com/matrix-org/synapse/issues/13129))
|
||||
- Apply ratelimiting earlier in processing of `/send` requests. ([\#13134](https://github.com/matrix-org/synapse/issues/13134))
|
||||
- Improve exception handling when processing events received over federation. ([\#13145](https://github.com/matrix-org/synapse/issues/13145))
|
||||
- Check that `auto_vacuum` is disabled when porting a SQLite database to Postgres, as `VACUUM`s must not be performed between runs of the script. ([\#13195](https://github.com/matrix-org/synapse/issues/13195))
|
||||
- Reduce DB usage of `/sync` when a large number of unread messages have recently been sent in a room. ([\#13119](https://github.com/matrix-org/synapse/issues/13119), [\#13153](https://github.com/matrix-org/synapse/issues/13153))
|
||||
- Reduce memory consumption when processing incoming events in large rooms. ([\#13078](https://github.com/matrix-org/synapse/issues/13078), [\#13222](https://github.com/matrix-org/synapse/issues/13222))
|
||||
- Reduce number of queries used to get profile information. Contributed by Nick @ Beeper (@fizzadar). ([\#13209](https://github.com/matrix-org/synapse/issues/13209))
|
||||
- Reduce number of events queried during room creation. Contributed by Nick @ Beeper (@fizzadar). ([\#13210](https://github.com/matrix-org/synapse/issues/13210))
|
||||
- More aggressively rotate push actions. ([\#13211](https://github.com/matrix-org/synapse/issues/13211))
|
||||
- Add `max_line_length` setting for Python files to the `.editorconfig`. Contributed by @sumnerevans @ Beeper. ([\#13228](https://github.com/matrix-org/synapse/issues/13228))
|
||||
|
||||
|
||||
Synapse 1.62.0 (2022-07-05)
|
||||
===========================
|
||||
|
||||
|
||||
12
book.toml
12
book.toml
@@ -34,14 +34,6 @@ additional-css = [
|
||||
"docs/website_files/table-of-contents.css",
|
||||
"docs/website_files/remove-nav-buttons.css",
|
||||
"docs/website_files/indent-section-headers.css",
|
||||
"docs/website_files/version-picker.css",
|
||||
]
|
||||
additional-js = [
|
||||
"docs/website_files/table-of-contents.js",
|
||||
"docs/website_files/version-picker.js",
|
||||
"docs/website_files/version.js",
|
||||
]
|
||||
theme = "docs/website_files/theme"
|
||||
|
||||
[preprocessor.schema_versions]
|
||||
command = "./scripts-dev/schema_versions.py"
|
||||
additional-js = ["docs/website_files/table-of-contents.js"]
|
||||
theme = "docs/website_files/theme"
|
||||
1
changelog.d/13028.misc
Normal file
1
changelog.d/13028.misc
Normal file
@@ -0,0 +1 @@
|
||||
Add type annotations to `tests.utils`.
|
||||
1
changelog.d/13029.doc
Normal file
1
changelog.d/13029.doc
Normal file
@@ -0,0 +1 @@
|
||||
Add an explanation of the `--report-stats` argument to the docs.
|
||||
1
changelog.d/13031.feature
Normal file
1
changelog.d/13031.feature
Normal file
@@ -0,0 +1 @@
|
||||
Implement [MSC3827](https://github.com/matrix-org/matrix-spec-proposals/pull/3827): Filtering of /publicRooms by room type.
|
||||
3
changelog.d/13077.doc
Normal file
3
changelog.d/13077.doc
Normal file
@@ -0,0 +1,3 @@
|
||||
Clean up references to sample configuration and redirect users to the configuration manual instead.
|
||||
|
||||
|
||||
1
changelog.d/13079.misc
Normal file
1
changelog.d/13079.misc
Normal file
@@ -0,0 +1 @@
|
||||
Enable Complement testing in the 'Twisted Trunk' CI runs.
|
||||
1
changelog.d/13086.doc
Normal file
1
changelog.d/13086.doc
Normal file
@@ -0,0 +1 @@
|
||||
Add documentation for anonymised homeserver statistics collection.
|
||||
1
changelog.d/13100.misc
Normal file
1
changelog.d/13100.misc
Normal file
@@ -0,0 +1 @@
|
||||
Faster room joins: Handle race between persisting an event and un-partial stating a room.
|
||||
1
changelog.d/13103.misc
Normal file
1
changelog.d/13103.misc
Normal file
@@ -0,0 +1 @@
|
||||
Add missing type hints to `synapse.logging`.
|
||||
1
changelog.d/13113.misc
Normal file
1
changelog.d/13113.misc
Normal file
@@ -0,0 +1 @@
|
||||
Raise a `DependencyError` on missing dependencies instead of a `ConfigError`.
|
||||
1
changelog.d/13116.doc
Normal file
1
changelog.d/13116.doc
Normal file
@@ -0,0 +1 @@
|
||||
Fix wrong section header for `allow_public_rooms_over_federation` in the homeserver config documentation.
|
||||
1
changelog.d/13119.misc
Normal file
1
changelog.d/13119.misc
Normal file
@@ -0,0 +1 @@
|
||||
Reduce DB usage of `/sync` when a large number of unread messages have recently been sent in a room.
|
||||
1
changelog.d/13125.feature
Normal file
1
changelog.d/13125.feature
Normal file
@@ -0,0 +1 @@
|
||||
Add a rate limit for local users sending invites.
|
||||
1
changelog.d/13127.misc
Normal file
1
changelog.d/13127.misc
Normal file
@@ -0,0 +1 @@
|
||||
Improve startup times in Complement test runs against workers, particularly in CPU-constrained environments.
|
||||
1
changelog.d/13129.misc
Normal file
1
changelog.d/13129.misc
Normal file
@@ -0,0 +1 @@
|
||||
Only one-line SQL statements for logging and tracing.
|
||||
1
changelog.d/13131.bugfix
Normal file
1
changelog.d/13131.bugfix
Normal file
@@ -0,0 +1 @@
|
||||
Fix application service not being able to join remote federated room without a profile set.
|
||||
1
changelog.d/13132.doc
Normal file
1
changelog.d/13132.doc
Normal file
@@ -0,0 +1 @@
|
||||
Document how the Synapse team does reviews.
|
||||
1
changelog.d/13134.misc
Normal file
1
changelog.d/13134.misc
Normal file
@@ -0,0 +1 @@
|
||||
Apply ratelimiting earlier in processing of /send request.
|
||||
1
changelog.d/13135.misc
Normal file
1
changelog.d/13135.misc
Normal file
@@ -0,0 +1 @@
|
||||
Enforce type annotations for `tests.test_server`.
|
||||
1
changelog.d/13136.misc
Normal file
1
changelog.d/13136.misc
Normal file
@@ -0,0 +1 @@
|
||||
Add type annotations to `tests.server`.
|
||||
1
changelog.d/13139.doc
Normal file
1
changelog.d/13139.doc
Normal file
@@ -0,0 +1 @@
|
||||
Add a link to the configuration manual from the homeserver sample config documentation.
|
||||
1
changelog.d/13143.misc
Normal file
1
changelog.d/13143.misc
Normal file
@@ -0,0 +1 @@
|
||||
Add support to `complement.sh` for skipping the docker build.
|
||||
1
changelog.d/13144.misc
Normal file
1
changelog.d/13144.misc
Normal file
@@ -0,0 +1 @@
|
||||
Faster joins: skip waiting for full state when processing incoming events over federation.
|
||||
1
changelog.d/13145.misc
Normal file
1
changelog.d/13145.misc
Normal file
@@ -0,0 +1 @@
|
||||
Improve exception handling when processing events received over federation.
|
||||
1
changelog.d/13148.feature
Normal file
1
changelog.d/13148.feature
Normal file
@@ -0,0 +1 @@
|
||||
Improve validation logic in Synapse's REST endpoints.
|
||||
1
changelog.d/13151.misc
Normal file
1
changelog.d/13151.misc
Normal file
@@ -0,0 +1 @@
|
||||
Faster room joins: fix race in recalculation of current room state.
|
||||
1
changelog.d/13152.misc
Normal file
1
changelog.d/13152.misc
Normal file
@@ -0,0 +1 @@
|
||||
Add the ability to set the log level using the `SYNAPSE_TEST_LOG_LEVEL` environment when using `complement.sh`.
|
||||
1
changelog.d/13153.misc
Normal file
1
changelog.d/13153.misc
Normal file
@@ -0,0 +1 @@
|
||||
Reduce DB usage of `/sync` when a large number of unread messages have recently been sent in a room.
|
||||
1
changelog.d/13157.misc
Normal file
1
changelog.d/13157.misc
Normal file
@@ -0,0 +1 @@
|
||||
Enable Complement testing in the 'Twisted Trunk' CI runs.
|
||||
1
changelog.d/13158.misc
Normal file
1
changelog.d/13158.misc
Normal file
@@ -0,0 +1 @@
|
||||
Add support to `complement.sh` for skipping the docker build.
|
||||
1
changelog.d/13159.misc
Normal file
1
changelog.d/13159.misc
Normal file
@@ -0,0 +1 @@
|
||||
Improve and fix type hints.
|
||||
1
changelog.d/13166.doc
Normal file
1
changelog.d/13166.doc
Normal file
@@ -0,0 +1 @@
|
||||
Add missing links to config options.
|
||||
1
changelog.d/13167.misc
Normal file
1
changelog.d/13167.misc
Normal file
@@ -0,0 +1 @@
|
||||
Update config used by Complement to allow device name lookup over federation.
|
||||
1
changelog.d/13174.bugfix
Normal file
1
changelog.d/13174.bugfix
Normal file
@@ -0,0 +1 @@
|
||||
Make use of the more robust `get_current_state` in `_get_state_map_for_room` to avoid breakages.
|
||||
1
changelog.d/13194.bugfix
Normal file
1
changelog.d/13194.bugfix
Normal file
@@ -0,0 +1 @@
|
||||
Fix bug where rows were not deleted from `event_push_actions` table on large servers. Introduced in v1.62.0.
|
||||
1
changelog.d/13195.misc
Normal file
1
changelog.d/13195.misc
Normal file
@@ -0,0 +1 @@
|
||||
Check that `auto_vacuum` is disabled when porting a SQLite database to Postgres, as `VACUUM`s must not be performed between runs of the script.
|
||||
1
changelog.d/13200.removal
Normal file
1
changelog.d/13200.removal
Normal file
@@ -0,0 +1 @@
|
||||
Remove obsolete and for 8 years unused `RoomEventsStoreTestCase`. Contributed by @arkamar.
|
||||
1
changelog.d/13207.docker
Normal file
1
changelog.d/13207.docker
Normal file
@@ -0,0 +1 @@
|
||||
Bump the version of `lxml` in matrix.org Docker images Debian packages from 4.8.0 to 4.9.1.
|
||||
1
changelog.d/13209.misc
Normal file
1
changelog.d/13209.misc
Normal file
@@ -0,0 +1 @@
|
||||
Reduce number of queries used to get profile information. Contributed by Nick @ Beeper (@fizzadar).
|
||||
@@ -1,31 +0,0 @@
|
||||
# Creating multiple workers with a bash script
|
||||
|
||||
Setting up multiple worker configuration files manually can be time-consuming.
|
||||
You can alternatively create multiple worker configuration files with a simple `bash` script. For example:
|
||||
|
||||
```sh
|
||||
#!/bin/bash
|
||||
for i in {1..5}
|
||||
do
|
||||
cat << EOF >> generic_worker$i.yaml
|
||||
worker_app: synapse.app.generic_worker
|
||||
worker_name: generic_worker$i
|
||||
|
||||
# The replication listener on the main synapse process.
|
||||
worker_replication_host: 127.0.0.1
|
||||
worker_replication_http_port: 9093
|
||||
|
||||
worker_listeners:
|
||||
- type: http
|
||||
port: 808$i
|
||||
resources:
|
||||
- names: [client, federation]
|
||||
|
||||
worker_log_config: /etc/matrix-synapse/generic-worker-log.yaml
|
||||
EOF
|
||||
done
|
||||
```
|
||||
|
||||
This would create five generic workers with a unique `worker_name` field in each file and listening on ports 8081-8085.
|
||||
|
||||
Customise the script to your needs.
|
||||
20
debian/changelog
vendored
20
debian/changelog
vendored
@@ -1,23 +1,3 @@
|
||||
matrix-synapse-py3 (1.63.1) stable; urgency=medium
|
||||
|
||||
* New Synapse release 1.63.1.
|
||||
|
||||
-- Synapse Packaging team <packages@matrix.org> Wed, 20 Jul 2022 13:36:52 +0100
|
||||
|
||||
matrix-synapse-py3 (1.63.0) stable; urgency=medium
|
||||
|
||||
* Clarify that homeserver server names are included in the data reported
|
||||
by opt-in server stats reporting (`report_stats` homeserver config option).
|
||||
* New Synapse release 1.63.0.
|
||||
|
||||
-- Synapse Packaging team <packages@matrix.org> Tue, 19 Jul 2022 14:42:24 +0200
|
||||
|
||||
matrix-synapse-py3 (1.63.0~rc1) stable; urgency=medium
|
||||
|
||||
* New Synapse release 1.63.0rc1.
|
||||
|
||||
-- Synapse Packaging team <packages@matrix.org> Tue, 12 Jul 2022 11:26:02 +0100
|
||||
|
||||
matrix-synapse-py3 (1.62.0) stable; urgency=medium
|
||||
|
||||
* New Synapse release 1.62.0.
|
||||
|
||||
2
debian/matrix-synapse-py3.postinst
vendored
2
debian/matrix-synapse-py3.postinst
vendored
@@ -31,7 +31,7 @@ EOF
|
||||
# This file is autogenerated, and will be recreated on upgrade if it is deleted.
|
||||
# Any changes you make will be preserved.
|
||||
|
||||
# Whether to report homeserver usage statistics.
|
||||
# Whether to report anonymized homeserver usage statistics.
|
||||
report_stats: false
|
||||
EOF
|
||||
fi
|
||||
|
||||
12
debian/po/templates.pot
vendored
12
debian/po/templates.pot
vendored
@@ -37,7 +37,7 @@ msgstr ""
|
||||
#. Type: boolean
|
||||
#. Description
|
||||
#: ../templates:2001
|
||||
msgid "Report homeserver usage statistics?"
|
||||
msgid "Report anonymous statistics?"
|
||||
msgstr ""
|
||||
|
||||
#. Type: boolean
|
||||
@@ -45,11 +45,11 @@ msgstr ""
|
||||
#: ../templates:2001
|
||||
msgid ""
|
||||
"Developers of Matrix and Synapse really appreciate helping the project out "
|
||||
"by reporting homeserver usage statistics from this homeserver. Your "
|
||||
"homeserver's server name, along with very basic aggregate data (e.g. "
|
||||
"number of users) will be reported. But it helps track the growth of the "
|
||||
"Matrix community, and helps in making Matrix a success, as well as to "
|
||||
"convince other networks that they should peer with Matrix."
|
||||
"by reporting anonymized usage statistics from this homeserver. Only very "
|
||||
"basic aggregate data (e.g. number of users) will be reported, but it helps "
|
||||
"track the growth of the Matrix community, and helps in making Matrix a "
|
||||
"success, as well as to convince other networks that they should peer with "
|
||||
"Matrix."
|
||||
msgstr ""
|
||||
|
||||
#. Type: boolean
|
||||
|
||||
13
debian/templates
vendored
13
debian/templates
vendored
@@ -10,13 +10,12 @@ _Description: Name of the server:
|
||||
Template: matrix-synapse/report-stats
|
||||
Type: boolean
|
||||
Default: false
|
||||
_Description: Report homeserver usage statistics?
|
||||
_Description: Report anonymous statistics?
|
||||
Developers of Matrix and Synapse really appreciate helping the
|
||||
project out by reporting homeserver usage statistics from this
|
||||
homeserver. Your homeserver's server name, along with very basic
|
||||
aggregate data (e.g. number of users) will be reported. But it
|
||||
helps track the growth of the Matrix community, and helps in
|
||||
making Matrix a success, as well as to convince other networks
|
||||
that they should peer with Matrix.
|
||||
project out by reporting anonymized usage statistics from this
|
||||
homeserver. Only very basic aggregate data (e.g. number of users)
|
||||
will be reported, but it helps track the growth of the Matrix
|
||||
community, and helps in making Matrix a success, as well as to
|
||||
convince other networks that they should peer with Matrix.
|
||||
.
|
||||
Thank you.
|
||||
|
||||
@@ -69,7 +69,7 @@
|
||||
- [Federation](usage/administration/admin_api/federation.md)
|
||||
- [Manhole](manhole.md)
|
||||
- [Monitoring](metrics-howto.md)
|
||||
- [Reporting Homeserver Usage Statistics](usage/administration/monitoring/reporting_homeserver_usage_statistics.md)
|
||||
- [Reporting Anonymised Statistics](usage/administration/monitoring/reporting_anonymised_statistics.md)
|
||||
- [Understanding Synapse Through Grafana Graphs](usage/administration/understanding_synapse_through_grafana_graphs.md)
|
||||
- [Useful SQL for Admins](usage/administration/useful_sql_for_admins.md)
|
||||
- [Database Maintenance Tools](usage/administration/database_maintenance_tools.md)
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
# Reporting Homeserver Usage Statistics
|
||||
# Reporting Anonymised Statistics
|
||||
|
||||
When generating your Synapse configuration file, you are asked whether you
|
||||
would like to report usage statistics to Matrix.org. These statistics
|
||||
would like to report anonymised statistics to Matrix.org. These statistics
|
||||
provide the foundation a glimpse into the number of Synapse homeservers
|
||||
participating in the network, as well as statistics such as the number of
|
||||
rooms being created and messages being sent. This feature is sometimes
|
||||
affectionately called "phone home" stats. Reporting
|
||||
affectionately called "phone-home" stats. Reporting
|
||||
[is optional](../../configuration/config_documentation.md#report_stats)
|
||||
and the reporting endpoint
|
||||
[can be configured](../../configuration/config_documentation.md#report_stats_endpoint),
|
||||
@@ -21,9 +21,9 @@ The following statistics are sent to the configured reporting endpoint:
|
||||
|
||||
| Statistic Name | Type | Description |
|
||||
|----------------------------|--------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
||||
| `homeserver` | string | The homeserver's server name. |
|
||||
| `memory_rss` | int | The memory usage of the process (in kilobytes on Unix-based systems, bytes on MacOS). |
|
||||
| `cpu_average` | int | CPU time in % of a single core (not % of all cores). |
|
||||
| `homeserver` | string | The homeserver's server name. |
|
||||
| `server_context` | string | An arbitrary string used to group statistics from a set of homeservers. |
|
||||
| `timestamp` | int | The current time, represented as the number of seconds since the epoch. |
|
||||
| `uptime_seconds` | int | The number of seconds since the homeserver was last started. |
|
||||
@@ -1257,98 +1257,6 @@ database:
|
||||
cp_max: 10
|
||||
```
|
||||
---
|
||||
### `databases`
|
||||
|
||||
The `databases` option allows specifying a mapping between certain database tables and
|
||||
database host details, spreading the load of a single Synapse instance across multiple
|
||||
database backends. This is often referred to as "database sharding". This option is only
|
||||
supported for PostgreSQL database backends.
|
||||
|
||||
**Important note:** This is a supported option, but is not currently used in production by the
|
||||
Matrix.org Foundation. Proceed with caution and always make backups.
|
||||
|
||||
`databases` is a dictionary of arbitrarily-named database entries. Each entry is equivalent
|
||||
to the value of the `database` homeserver config option (see above), with the addition of
|
||||
a `data_stores` key. `data_stores` is an array of strings that specifies the data store(s)
|
||||
(a defined label for a set of tables) that should be stored on the associated database
|
||||
backend entry.
|
||||
|
||||
The currently defined values for `data_stores` are:
|
||||
|
||||
* `"state"`: Database that relates to state groups will be stored in this database.
|
||||
|
||||
Specifically, that means the following tables:
|
||||
* `state_groups`
|
||||
* `state_group_edges`
|
||||
* `state_groups_state`
|
||||
|
||||
And the following sequences:
|
||||
* `state_groups_seq_id`
|
||||
|
||||
* `"main"`: All other database tables and sequences.
|
||||
|
||||
All databases will end up with additional tables used for tracking database schema migrations
|
||||
and any pending background updates. Synapse will create these automatically on startup when checking for
|
||||
and/or performing database schema migrations.
|
||||
|
||||
To migrate an existing database configuration (e.g. all tables on a single database) to a different
|
||||
configuration (e.g. the "main" data store on one database, and "state" on another), do the following:
|
||||
|
||||
1. Take a backup of your existing database. Things can and do go wrong and database corruption is no joke!
|
||||
2. Ensure all pending database migrations have been applied and background updates have run. The simplest
|
||||
way to do this is to use the `update_synapse_database` script supplied with your Synapse installation.
|
||||
|
||||
```sh
|
||||
update_synapse_database --database-config homeserver.yaml --run-background-updates
|
||||
```
|
||||
|
||||
3. Copy over the necessary tables and sequences from one database to the other. Tables relating to database
|
||||
migrations, schemas, schema versions and background updates should **not** be copied.
|
||||
|
||||
As an example, say that you'd like to split out the "state" data store from an existing database which
|
||||
currently contains all data stores.
|
||||
|
||||
Simply copy the tables and sequences defined above for the "state" datastore from the existing database
|
||||
to the secondary database. As noted above, additional tables will be created in the secondary database
|
||||
when Synapse is started.
|
||||
|
||||
4. Modify/create the `databases` option in your `homeserver.yaml` to match the desired database configuration.
|
||||
5. Start Synapse. Check that it starts up successfully and that things generally seem to be working.
|
||||
6. Drop the old tables that were copied in step 3.
|
||||
|
||||
Only one of the options `database` or `databases` may be specified in your config, but not both.
|
||||
|
||||
Example configuration:
|
||||
|
||||
```yaml
|
||||
databases:
|
||||
basement_box:
|
||||
name: psycopg2
|
||||
txn_limit: 10000
|
||||
data_stores: ["main"]
|
||||
args:
|
||||
user: synapse_user
|
||||
password: secretpassword
|
||||
database: synapse_main
|
||||
host: localhost
|
||||
port: 5432
|
||||
cp_min: 5
|
||||
cp_max: 10
|
||||
|
||||
my_other_database:
|
||||
name: psycopg2
|
||||
txn_limit: 10000
|
||||
data_stores: ["state"]
|
||||
args:
|
||||
user: synapse_user
|
||||
password: secretpassword
|
||||
database: synapse_state
|
||||
host: localhost
|
||||
port: 5432
|
||||
cp_min: 5
|
||||
cp_max: 10
|
||||
```
|
||||
---
|
||||
## Logging ##
|
||||
Config options related to logging.
|
||||
|
||||
@@ -2409,14 +2317,9 @@ metrics_flags:
|
||||
---
|
||||
### `report_stats`
|
||||
|
||||
Whether or not to report homeserver usage statistics. This is originally
|
||||
Whether or not to report anonymized homeserver usage statistics. This is originally
|
||||
set when generating the config. Set this option to true or false to change the current
|
||||
behavior. See
|
||||
[Reporting Homeserver Usage Statistics](../administration/monitoring/reporting_homeserver_usage_statistics.md)
|
||||
for information on what data is reported.
|
||||
|
||||
Statistics will be reported 5 minutes after Synapse starts, and then every 3 hours
|
||||
after that.
|
||||
behavior.
|
||||
|
||||
Example configuration:
|
||||
```yaml
|
||||
@@ -2425,7 +2328,7 @@ report_stats: true
|
||||
---
|
||||
### `report_stats_endpoint`
|
||||
|
||||
The endpoint to report homeserver usage statistics to.
|
||||
The endpoint to report the anonymized homeserver usage statistics to.
|
||||
Defaults to https://matrix.org/report-usage-stats/push
|
||||
|
||||
Example configuration:
|
||||
|
||||
@@ -24,11 +24,6 @@ Finally, we also stylise the chapter titles in the left sidebar by indenting the
|
||||
slightly so that they are more visually distinguishable from the section headers
|
||||
(the bold titles). This is done through the `indent-section-headers.css` file.
|
||||
|
||||
In addition to these modifications, we have added a version picker to the documentation.
|
||||
Users can switch between documentations for different versions of Synapse.
|
||||
This functionality was implemented through the `version-picker.js` and
|
||||
`version-picker.css` files.
|
||||
|
||||
More information can be found in mdbook's official documentation for
|
||||
[injecting page JS/CSS](https://rust-lang.github.io/mdBook/format/config.html)
|
||||
and
|
||||
|
||||
@@ -131,18 +131,6 @@
|
||||
<i class="fa fa-search"></i>
|
||||
</button>
|
||||
{{/if}}
|
||||
<div class="version-picker">
|
||||
<div class="dropdown">
|
||||
<div class="select">
|
||||
<span></span>
|
||||
<i class="fa fa-chevron-down"></i>
|
||||
</div>
|
||||
<input type="hidden" name="version">
|
||||
<ul class="dropdown-menu">
|
||||
<!-- Versions will be added dynamically in version-picker.js -->
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h1 class="menu-title">{{ book_title }}</h1>
|
||||
@@ -321,4 +309,4 @@
|
||||
{{/if}}
|
||||
|
||||
</body>
|
||||
</html>
|
||||
</html>
|
||||
@@ -1,78 +0,0 @@
|
||||
.version-picker {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.version-picker .dropdown {
|
||||
width: 130px;
|
||||
max-height: 29px;
|
||||
margin-left: 10px;
|
||||
display: inline-block;
|
||||
border-radius: 4px;
|
||||
border: 1px solid var(--theme-popup-border);
|
||||
position: relative;
|
||||
font-size: 13px;
|
||||
color: var(--fg);
|
||||
height: 100%;
|
||||
text-align: left;
|
||||
}
|
||||
.version-picker .dropdown .select {
|
||||
cursor: pointer;
|
||||
display: block;
|
||||
padding: 5px 2px 5px 15px;
|
||||
}
|
||||
.version-picker .dropdown .select > i {
|
||||
font-size: 10px;
|
||||
color: var(--fg);
|
||||
cursor: pointer;
|
||||
float: right;
|
||||
line-height: 20px !important;
|
||||
}
|
||||
.version-picker .dropdown:hover {
|
||||
border: 1px solid var(--theme-popup-border);
|
||||
}
|
||||
.version-picker .dropdown:active {
|
||||
background-color: var(--theme-popup-bg);
|
||||
}
|
||||
.version-picker .dropdown.active:hover,
|
||||
.version-picker .dropdown.active {
|
||||
border: 1px solid var(--theme-popup-border);
|
||||
border-radius: 2px 2px 0 0;
|
||||
background-color: var(--theme-popup-bg);
|
||||
}
|
||||
.version-picker .dropdown.active .select > i {
|
||||
transform: rotate(-180deg);
|
||||
}
|
||||
.version-picker .dropdown .dropdown-menu {
|
||||
position: absolute;
|
||||
background-color: var(--theme-popup-bg);
|
||||
width: 100%;
|
||||
left: -1px;
|
||||
right: 1px;
|
||||
margin-top: 1px;
|
||||
border: 1px solid var(--theme-popup-border);
|
||||
border-radius: 0 0 4px 4px;
|
||||
overflow: hidden;
|
||||
display: none;
|
||||
max-height: 300px;
|
||||
overflow-y: auto;
|
||||
z-index: 9;
|
||||
}
|
||||
.version-picker .dropdown .dropdown-menu li {
|
||||
font-size: 12px;
|
||||
padding: 6px 20px;
|
||||
cursor: pointer;
|
||||
}
|
||||
.version-picker .dropdown .dropdown-menu {
|
||||
padding: 0;
|
||||
list-style: none;
|
||||
}
|
||||
.version-picker .dropdown .dropdown-menu li:hover {
|
||||
background-color: var(--theme-hover);
|
||||
}
|
||||
.version-picker .dropdown .dropdown-menu li.active::before {
|
||||
display: inline-block;
|
||||
content: "✓";
|
||||
margin-inline-start: -14px;
|
||||
width: 14px;
|
||||
}
|
||||
@@ -1,127 +0,0 @@
|
||||
|
||||
const dropdown = document.querySelector('.version-picker .dropdown');
|
||||
const dropdownMenu = dropdown.querySelector('.dropdown-menu');
|
||||
|
||||
fetchVersions(dropdown, dropdownMenu).then(() => {
|
||||
initializeVersionDropdown(dropdown, dropdownMenu);
|
||||
});
|
||||
|
||||
/**
|
||||
* Initialize the dropdown functionality for version selection.
|
||||
*
|
||||
* @param {Element} dropdown - The dropdown element.
|
||||
* @param {Element} dropdownMenu - The dropdown menu element.
|
||||
*/
|
||||
function initializeVersionDropdown(dropdown, dropdownMenu) {
|
||||
// Toggle the dropdown menu on click
|
||||
dropdown.addEventListener('click', function () {
|
||||
this.setAttribute('tabindex', 1);
|
||||
this.classList.toggle('active');
|
||||
dropdownMenu.style.display = (dropdownMenu.style.display === 'block') ? 'none' : 'block';
|
||||
});
|
||||
|
||||
// Remove the 'active' class and hide the dropdown menu on focusout
|
||||
dropdown.addEventListener('focusout', function () {
|
||||
this.classList.remove('active');
|
||||
dropdownMenu.style.display = 'none';
|
||||
});
|
||||
|
||||
// Handle item selection within the dropdown menu
|
||||
const dropdownMenuItems = dropdownMenu.querySelectorAll('li');
|
||||
dropdownMenuItems.forEach(function (item) {
|
||||
item.addEventListener('click', function () {
|
||||
dropdownMenuItems.forEach(function (item) {
|
||||
item.classList.remove('active');
|
||||
});
|
||||
this.classList.add('active');
|
||||
dropdown.querySelector('span').textContent = this.textContent;
|
||||
dropdown.querySelector('input').value = this.getAttribute('id');
|
||||
|
||||
window.location.href = changeVersion(window.location.href, this.textContent);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* This function fetches the available versions from a GitHub repository
|
||||
* and inserts them into the version picker.
|
||||
*
|
||||
* @param {Element} dropdown - The dropdown element.
|
||||
* @param {Element} dropdownMenu - The dropdown menu element.
|
||||
* @returns {Promise<Array<string>>} A promise that resolves with an array of available versions.
|
||||
*/
|
||||
function fetchVersions(dropdown, dropdownMenu) {
|
||||
return new Promise((resolve, reject) => {
|
||||
window.addEventListener("load", () => {
|
||||
|
||||
fetch("https://api.github.com/repos/matrix-org/synapse/git/trees/gh-pages", {
|
||||
cache: "force-cache",
|
||||
}).then(res =>
|
||||
res.json()
|
||||
).then(resObject => {
|
||||
const excluded = ['dev-docs', 'v1.91.0', 'v1.80.0', 'v1.69.0'];
|
||||
const tree = resObject.tree.filter(item => item.type === "tree" && !excluded.includes(item.path));
|
||||
const versions = tree.map(item => item.path).sort(sortVersions);
|
||||
|
||||
// Create a list of <li> items for versions
|
||||
versions.forEach((version) => {
|
||||
const li = document.createElement("li");
|
||||
li.textContent = version;
|
||||
li.id = version;
|
||||
|
||||
if (window.SYNAPSE_VERSION === version) {
|
||||
li.classList.add('active');
|
||||
dropdown.querySelector('span').textContent = version;
|
||||
dropdown.querySelector('input').value = version;
|
||||
}
|
||||
|
||||
dropdownMenu.appendChild(li);
|
||||
});
|
||||
|
||||
resolve(versions);
|
||||
|
||||
}).catch(ex => {
|
||||
console.error("Failed to fetch version data", ex);
|
||||
reject(ex);
|
||||
})
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Custom sorting function to sort an array of version strings.
|
||||
*
|
||||
* @param {string} a - The first version string to compare.
|
||||
* @param {string} b - The second version string to compare.
|
||||
* @returns {number} - A negative number if a should come before b, a positive number if b should come before a, or 0 if they are equal.
|
||||
*/
|
||||
function sortVersions(a, b) {
|
||||
// Put 'develop' and 'latest' at the top
|
||||
if (a === 'develop' || a === 'latest') return -1;
|
||||
if (b === 'develop' || b === 'latest') return 1;
|
||||
|
||||
const versionA = (a.match(/v\d+(\.\d+)+/) || [])[0];
|
||||
const versionB = (b.match(/v\d+(\.\d+)+/) || [])[0];
|
||||
|
||||
return versionB.localeCompare(versionA);
|
||||
}
|
||||
|
||||
/**
|
||||
* Change the version in a URL path.
|
||||
*
|
||||
* @param {string} url - The original URL to be modified.
|
||||
* @param {string} newVersion - The new version to replace the existing version in the URL.
|
||||
* @returns {string} The updated URL with the new version.
|
||||
*/
|
||||
function changeVersion(url, newVersion) {
|
||||
const parsedURL = new URL(url);
|
||||
const pathSegments = parsedURL.pathname.split('/');
|
||||
|
||||
// Modify the version
|
||||
pathSegments[2] = newVersion;
|
||||
|
||||
// Reconstruct the URL
|
||||
parsedURL.pathname = pathSegments.join('/');
|
||||
|
||||
return parsedURL.href;
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
window.SYNAPSE_VERSION = 'v1.63';
|
||||
@@ -54,7 +54,7 @@ skip_gitignore = true
|
||||
|
||||
[tool.poetry]
|
||||
name = "matrix-synapse"
|
||||
version = "1.63.1"
|
||||
version = "1.62.0"
|
||||
description = "Homeserver for the Matrix decentralised comms protocol"
|
||||
authors = ["Matrix.org Team and Contributors <packages@matrix.org>"]
|
||||
license = "Apache-2.0"
|
||||
|
||||
@@ -33,7 +33,7 @@ def main() -> None:
|
||||
parser.add_argument(
|
||||
"--report-stats",
|
||||
action="store",
|
||||
help="Whether the generated config reports homeserver usage statistics",
|
||||
help="Whether the generated config reports anonymized usage statistics",
|
||||
choices=["yes", "no"],
|
||||
)
|
||||
|
||||
|
||||
@@ -418,15 +418,12 @@ class Porter:
|
||||
self.progress.update(table, table_size) # Mark table as done
|
||||
return
|
||||
|
||||
# We sweep over rowids in two directions: one forwards (rowids 1, 2, 3, ...)
|
||||
# and another backwards (rowids 0, -1, -2, ...).
|
||||
forward_select = (
|
||||
"SELECT rowid, * FROM %s WHERE rowid >= ? ORDER BY rowid LIMIT ?" % (table,)
|
||||
)
|
||||
|
||||
backward_select = (
|
||||
"SELECT rowid, * FROM %s WHERE rowid <= ? ORDER BY rowid DESC LIMIT ?"
|
||||
% (table,)
|
||||
"SELECT rowid, * FROM %s WHERE rowid <= ? ORDER BY rowid LIMIT ?" % (table,)
|
||||
)
|
||||
|
||||
do_forward = [True]
|
||||
|
||||
@@ -297,14 +297,8 @@ class AuthError(SynapseError):
|
||||
other poorly-defined times.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
code: int,
|
||||
msg: str,
|
||||
errcode: str = Codes.FORBIDDEN,
|
||||
additional_fields: Optional[dict] = None,
|
||||
):
|
||||
super().__init__(code, msg, errcode, additional_fields)
|
||||
def __init__(self, code: int, msg: str, errcode: str = Codes.FORBIDDEN):
|
||||
super().__init__(code, msg, errcode)
|
||||
|
||||
|
||||
class InvalidClientCredentialsError(SynapseError):
|
||||
|
||||
@@ -39,7 +39,6 @@ from synapse.replication.slave.storage.push_rule import SlavedPushRuleStore
|
||||
from synapse.replication.slave.storage.receipts import SlavedReceiptsStore
|
||||
from synapse.replication.slave.storage.registration import SlavedRegistrationStore
|
||||
from synapse.server import HomeServer
|
||||
from synapse.storage.database import DatabasePool, LoggingDatabaseConnection
|
||||
from synapse.storage.databases.main.room import RoomWorkerStore
|
||||
from synapse.types import StateMap
|
||||
from synapse.util import SYNAPSE_VERSION
|
||||
@@ -61,17 +60,7 @@ class AdminCmdSlavedStore(
|
||||
BaseSlavedStore,
|
||||
RoomWorkerStore,
|
||||
):
|
||||
def __init__(
|
||||
self,
|
||||
database: DatabasePool,
|
||||
db_conn: LoggingDatabaseConnection,
|
||||
hs: "HomeServer",
|
||||
):
|
||||
super().__init__(database, db_conn, hs)
|
||||
|
||||
# Annoyingly `filter_events_for_client` assumes that this exists. We
|
||||
# should refactor it to take a `Clock` directly.
|
||||
self.clock = hs.get_clock()
|
||||
pass
|
||||
|
||||
|
||||
class AdminCmdServer(HomeServer):
|
||||
|
||||
@@ -319,9 +319,7 @@ class _ServiceQueuer:
|
||||
rooms_of_interesting_users.update(event.room_id for event in events)
|
||||
# EDUs
|
||||
rooms_of_interesting_users.update(
|
||||
ephemeral["room_id"]
|
||||
for ephemeral in ephemerals
|
||||
if ephemeral.get("room_id") is not None
|
||||
ephemeral["room_id"] for ephemeral in ephemerals
|
||||
)
|
||||
|
||||
# Look up the AS users in those rooms
|
||||
@@ -331,9 +329,8 @@ class _ServiceQueuer:
|
||||
)
|
||||
|
||||
# Add recipients of to-device messages.
|
||||
users.update(
|
||||
device_message["to_user_id"] for device_message in to_device_messages
|
||||
)
|
||||
# device_message["user_id"] is the ID of the recipient.
|
||||
users.update(device_message["user_id"] for device_message in to_device_messages)
|
||||
|
||||
# Compute and return the counts / fallback key usage states
|
||||
otk_counts = await self._store.count_bulk_e2e_one_time_keys_for_as(users)
|
||||
|
||||
@@ -97,16 +97,16 @@ def format_config_error(e: ConfigError) -> Iterator[str]:
|
||||
# We split these messages out to allow packages to override with package
|
||||
# specific instructions.
|
||||
MISSING_REPORT_STATS_CONFIG_INSTRUCTIONS = """\
|
||||
Please opt in or out of reporting homeserver usage statistics, by setting
|
||||
the `report_stats` key in your config file to either True or False.
|
||||
Please opt in or out of reporting anonymized homeserver usage statistics, by
|
||||
setting the `report_stats` key in your config file to either True or False.
|
||||
"""
|
||||
|
||||
MISSING_REPORT_STATS_SPIEL = """\
|
||||
We would really appreciate it if you could help our project out by reporting
|
||||
homeserver usage statistics from your homeserver. Your homeserver's server name,
|
||||
along with very basic aggregate data (e.g. number of users) will be reported. But
|
||||
it helps us to track the growth of the Matrix community, and helps us to make Matrix
|
||||
a success, as well as to convince other networks that they should peer with us.
|
||||
anonymized usage statistics from your homeserver. Only very basic aggregate
|
||||
data (e.g. number of users) will be reported, but it helps us to track the
|
||||
growth of the Matrix community, and helps us to make Matrix a success, as well
|
||||
as to convince other networks that they should peer with us.
|
||||
|
||||
Thank you.
|
||||
"""
|
||||
@@ -621,7 +621,7 @@ class RootConfig:
|
||||
generate_group.add_argument(
|
||||
"--report-stats",
|
||||
action="store",
|
||||
help="Whether the generated config reports homeserver usage statistics.",
|
||||
help="Whether the generated config reports anonymized usage statistics.",
|
||||
choices=["yes", "no"],
|
||||
)
|
||||
generate_group.add_argument(
|
||||
|
||||
@@ -21,6 +21,7 @@ from typing import (
|
||||
Awaitable,
|
||||
Callable,
|
||||
Collection,
|
||||
Dict,
|
||||
List,
|
||||
Optional,
|
||||
Tuple,
|
||||
@@ -31,11 +32,10 @@ from typing import (
|
||||
from typing_extensions import Literal
|
||||
|
||||
import synapse
|
||||
from synapse.api.errors import Codes
|
||||
from synapse.rest.media.v1._base import FileInfo
|
||||
from synapse.rest.media.v1.media_storage import ReadableFileWrapper
|
||||
from synapse.spam_checker_api import RegistrationBehaviour
|
||||
from synapse.types import JsonDict, RoomAlias, UserProfile
|
||||
from synapse.types import RoomAlias, UserProfile
|
||||
from synapse.util.async_helpers import delay_cancellation, maybe_awaitable
|
||||
from synapse.util.metrics import Measure
|
||||
|
||||
@@ -50,12 +50,12 @@ CHECK_EVENT_FOR_SPAM_CALLBACK = Callable[
|
||||
Awaitable[
|
||||
Union[
|
||||
str,
|
||||
Codes,
|
||||
"synapse.api.errors.Codes",
|
||||
# Highly experimental, not officially part of the spamchecker API, may
|
||||
# disappear without warning depending on the results of ongoing
|
||||
# experiments.
|
||||
# Use this to return additional information as part of an error.
|
||||
Tuple[Codes, JsonDict],
|
||||
Tuple["synapse.api.errors.Codes", Dict],
|
||||
# Deprecated
|
||||
bool,
|
||||
]
|
||||
@@ -70,12 +70,7 @@ USER_MAY_JOIN_ROOM_CALLBACK = Callable[
|
||||
Awaitable[
|
||||
Union[
|
||||
Literal["NOT_SPAM"],
|
||||
Codes,
|
||||
# Highly experimental, not officially part of the spamchecker API, may
|
||||
# disappear without warning depending on the results of ongoing
|
||||
# experiments.
|
||||
# Use this to return additional information as part of an error.
|
||||
Tuple[Codes, JsonDict],
|
||||
"synapse.api.errors.Codes",
|
||||
# Deprecated
|
||||
bool,
|
||||
]
|
||||
@@ -86,12 +81,7 @@ USER_MAY_INVITE_CALLBACK = Callable[
|
||||
Awaitable[
|
||||
Union[
|
||||
Literal["NOT_SPAM"],
|
||||
Codes,
|
||||
# Highly experimental, not officially part of the spamchecker API, may
|
||||
# disappear without warning depending on the results of ongoing
|
||||
# experiments.
|
||||
# Use this to return additional information as part of an error.
|
||||
Tuple[Codes, JsonDict],
|
||||
"synapse.api.errors.Codes",
|
||||
# Deprecated
|
||||
bool,
|
||||
]
|
||||
@@ -102,12 +92,7 @@ USER_MAY_SEND_3PID_INVITE_CALLBACK = Callable[
|
||||
Awaitable[
|
||||
Union[
|
||||
Literal["NOT_SPAM"],
|
||||
Codes,
|
||||
# Highly experimental, not officially part of the spamchecker API, may
|
||||
# disappear without warning depending on the results of ongoing
|
||||
# experiments.
|
||||
# Use this to return additional information as part of an error.
|
||||
Tuple[Codes, JsonDict],
|
||||
"synapse.api.errors.Codes",
|
||||
# Deprecated
|
||||
bool,
|
||||
]
|
||||
@@ -118,12 +103,7 @@ USER_MAY_CREATE_ROOM_CALLBACK = Callable[
|
||||
Awaitable[
|
||||
Union[
|
||||
Literal["NOT_SPAM"],
|
||||
Codes,
|
||||
# Highly experimental, not officially part of the spamchecker API, may
|
||||
# disappear without warning depending on the results of ongoing
|
||||
# experiments.
|
||||
# Use this to return additional information as part of an error.
|
||||
Tuple[Codes, JsonDict],
|
||||
"synapse.api.errors.Codes",
|
||||
# Deprecated
|
||||
bool,
|
||||
]
|
||||
@@ -134,12 +114,7 @@ USER_MAY_CREATE_ROOM_ALIAS_CALLBACK = Callable[
|
||||
Awaitable[
|
||||
Union[
|
||||
Literal["NOT_SPAM"],
|
||||
Codes,
|
||||
# Highly experimental, not officially part of the spamchecker API, may
|
||||
# disappear without warning depending on the results of ongoing
|
||||
# experiments.
|
||||
# Use this to return additional information as part of an error.
|
||||
Tuple[Codes, JsonDict],
|
||||
"synapse.api.errors.Codes",
|
||||
# Deprecated
|
||||
bool,
|
||||
]
|
||||
@@ -150,12 +125,7 @@ USER_MAY_PUBLISH_ROOM_CALLBACK = Callable[
|
||||
Awaitable[
|
||||
Union[
|
||||
Literal["NOT_SPAM"],
|
||||
Codes,
|
||||
# Highly experimental, not officially part of the spamchecker API, may
|
||||
# disappear without warning depending on the results of ongoing
|
||||
# experiments.
|
||||
# Use this to return additional information as part of an error.
|
||||
Tuple[Codes, JsonDict],
|
||||
"synapse.api.errors.Codes",
|
||||
# Deprecated
|
||||
bool,
|
||||
]
|
||||
@@ -184,12 +154,7 @@ CHECK_MEDIA_FILE_FOR_SPAM_CALLBACK = Callable[
|
||||
Awaitable[
|
||||
Union[
|
||||
Literal["NOT_SPAM"],
|
||||
Codes,
|
||||
# Highly experimental, not officially part of the spamchecker API, may
|
||||
# disappear without warning depending on the results of ongoing
|
||||
# experiments.
|
||||
# Use this to return additional information as part of an error.
|
||||
Tuple[Codes, JsonDict],
|
||||
"synapse.api.errors.Codes",
|
||||
# Deprecated
|
||||
bool,
|
||||
]
|
||||
@@ -380,7 +345,7 @@ class SpamChecker:
|
||||
|
||||
async def check_event_for_spam(
|
||||
self, event: "synapse.events.EventBase"
|
||||
) -> Union[Tuple[Codes, JsonDict], str]:
|
||||
) -> Union[Tuple["synapse.api.errors.Codes", Dict], str]:
|
||||
"""Checks if a given event is considered "spammy" by this server.
|
||||
|
||||
If the server considers an event spammy, then it will be rejected if
|
||||
@@ -411,16 +376,7 @@ class SpamChecker:
|
||||
elif res is True:
|
||||
# This spam-checker rejects the event with deprecated
|
||||
# return value `True`
|
||||
return synapse.api.errors.Codes.FORBIDDEN, {}
|
||||
elif (
|
||||
isinstance(res, tuple)
|
||||
and len(res) == 2
|
||||
and isinstance(res[0], synapse.api.errors.Codes)
|
||||
and isinstance(res[1], dict)
|
||||
):
|
||||
return res
|
||||
elif isinstance(res, synapse.api.errors.Codes):
|
||||
return res, {}
|
||||
return (synapse.api.errors.Codes.FORBIDDEN, {})
|
||||
elif not isinstance(res, str):
|
||||
# mypy complains that we can't reach this code because of the
|
||||
# return type in CHECK_EVENT_FOR_SPAM_CALLBACK, but we don't know
|
||||
@@ -466,7 +422,7 @@ class SpamChecker:
|
||||
|
||||
async def user_may_join_room(
|
||||
self, user_id: str, room_id: str, is_invited: bool
|
||||
) -> Union[Tuple[Codes, JsonDict], Literal["NOT_SPAM"]]:
|
||||
) -> Union["synapse.api.errors.Codes", Literal["NOT_SPAM"]]:
|
||||
"""Checks if a given users is allowed to join a room.
|
||||
Not called when a user creates a room.
|
||||
|
||||
@@ -476,7 +432,7 @@ class SpamChecker:
|
||||
is_invited: Whether the user is invited into the room
|
||||
|
||||
Returns:
|
||||
NOT_SPAM if the operation is permitted, [Codes, Dict] otherwise.
|
||||
NOT_SPAM if the operation is permitted, Codes otherwise.
|
||||
"""
|
||||
for callback in self._user_may_join_room_callbacks:
|
||||
with Measure(
|
||||
@@ -487,28 +443,21 @@ class SpamChecker:
|
||||
if res is True or res is self.NOT_SPAM:
|
||||
continue
|
||||
elif res is False:
|
||||
return synapse.api.errors.Codes.FORBIDDEN, {}
|
||||
return synapse.api.errors.Codes.FORBIDDEN
|
||||
elif isinstance(res, synapse.api.errors.Codes):
|
||||
return res, {}
|
||||
elif (
|
||||
isinstance(res, tuple)
|
||||
and len(res) == 2
|
||||
and isinstance(res[0], synapse.api.errors.Codes)
|
||||
and isinstance(res[1], dict)
|
||||
):
|
||||
return res
|
||||
else:
|
||||
logger.warning(
|
||||
"Module returned invalid value, rejecting join as spam"
|
||||
)
|
||||
return synapse.api.errors.Codes.FORBIDDEN, {}
|
||||
return synapse.api.errors.Codes.FORBIDDEN
|
||||
|
||||
# No spam-checker has rejected the request, let it pass.
|
||||
return self.NOT_SPAM
|
||||
|
||||
async def user_may_invite(
|
||||
self, inviter_userid: str, invitee_userid: str, room_id: str
|
||||
) -> Union[Tuple[Codes, dict], Literal["NOT_SPAM"]]:
|
||||
) -> Union["synapse.api.errors.Codes", Literal["NOT_SPAM"]]:
|
||||
"""Checks if a given user may send an invite
|
||||
|
||||
Args:
|
||||
@@ -530,28 +479,21 @@ class SpamChecker:
|
||||
if res is True or res is self.NOT_SPAM:
|
||||
continue
|
||||
elif res is False:
|
||||
return synapse.api.errors.Codes.FORBIDDEN, {}
|
||||
return synapse.api.errors.Codes.FORBIDDEN
|
||||
elif isinstance(res, synapse.api.errors.Codes):
|
||||
return res, {}
|
||||
elif (
|
||||
isinstance(res, tuple)
|
||||
and len(res) == 2
|
||||
and isinstance(res[0], synapse.api.errors.Codes)
|
||||
and isinstance(res[1], dict)
|
||||
):
|
||||
return res
|
||||
else:
|
||||
logger.warning(
|
||||
"Module returned invalid value, rejecting invite as spam"
|
||||
)
|
||||
return synapse.api.errors.Codes.FORBIDDEN, {}
|
||||
return synapse.api.errors.Codes.FORBIDDEN
|
||||
|
||||
# No spam-checker has rejected the request, let it pass.
|
||||
return self.NOT_SPAM
|
||||
|
||||
async def user_may_send_3pid_invite(
|
||||
self, inviter_userid: str, medium: str, address: str, room_id: str
|
||||
) -> Union[Tuple[Codes, dict], Literal["NOT_SPAM"]]:
|
||||
) -> Union["synapse.api.errors.Codes", Literal["NOT_SPAM"]]:
|
||||
"""Checks if a given user may invite a given threepid into the room
|
||||
|
||||
Note that if the threepid is already associated with a Matrix user ID, Synapse
|
||||
@@ -577,27 +519,20 @@ class SpamChecker:
|
||||
if res is True or res is self.NOT_SPAM:
|
||||
continue
|
||||
elif res is False:
|
||||
return synapse.api.errors.Codes.FORBIDDEN, {}
|
||||
return synapse.api.errors.Codes.FORBIDDEN
|
||||
elif isinstance(res, synapse.api.errors.Codes):
|
||||
return res, {}
|
||||
elif (
|
||||
isinstance(res, tuple)
|
||||
and len(res) == 2
|
||||
and isinstance(res[0], synapse.api.errors.Codes)
|
||||
and isinstance(res[1], dict)
|
||||
):
|
||||
return res
|
||||
else:
|
||||
logger.warning(
|
||||
"Module returned invalid value, rejecting 3pid invite as spam"
|
||||
)
|
||||
return synapse.api.errors.Codes.FORBIDDEN, {}
|
||||
return synapse.api.errors.Codes.FORBIDDEN
|
||||
|
||||
return self.NOT_SPAM
|
||||
|
||||
async def user_may_create_room(
|
||||
self, userid: str
|
||||
) -> Union[Tuple[Codes, dict], Literal["NOT_SPAM"]]:
|
||||
) -> Union["synapse.api.errors.Codes", Literal["NOT_SPAM"]]:
|
||||
"""Checks if a given user may create a room
|
||||
|
||||
Args:
|
||||
@@ -611,27 +546,20 @@ class SpamChecker:
|
||||
if res is True or res is self.NOT_SPAM:
|
||||
continue
|
||||
elif res is False:
|
||||
return synapse.api.errors.Codes.FORBIDDEN, {}
|
||||
return synapse.api.errors.Codes.FORBIDDEN
|
||||
elif isinstance(res, synapse.api.errors.Codes):
|
||||
return res, {}
|
||||
elif (
|
||||
isinstance(res, tuple)
|
||||
and len(res) == 2
|
||||
and isinstance(res[0], synapse.api.errors.Codes)
|
||||
and isinstance(res[1], dict)
|
||||
):
|
||||
return res
|
||||
else:
|
||||
logger.warning(
|
||||
"Module returned invalid value, rejecting room creation as spam"
|
||||
)
|
||||
return synapse.api.errors.Codes.FORBIDDEN, {}
|
||||
return synapse.api.errors.Codes.FORBIDDEN
|
||||
|
||||
return self.NOT_SPAM
|
||||
|
||||
async def user_may_create_room_alias(
|
||||
self, userid: str, room_alias: RoomAlias
|
||||
) -> Union[Tuple[Codes, dict], Literal["NOT_SPAM"]]:
|
||||
) -> Union["synapse.api.errors.Codes", Literal["NOT_SPAM"]]:
|
||||
"""Checks if a given user may create a room alias
|
||||
|
||||
Args:
|
||||
@@ -647,27 +575,20 @@ class SpamChecker:
|
||||
if res is True or res is self.NOT_SPAM:
|
||||
continue
|
||||
elif res is False:
|
||||
return synapse.api.errors.Codes.FORBIDDEN, {}
|
||||
return synapse.api.errors.Codes.FORBIDDEN
|
||||
elif isinstance(res, synapse.api.errors.Codes):
|
||||
return res, {}
|
||||
elif (
|
||||
isinstance(res, tuple)
|
||||
and len(res) == 2
|
||||
and isinstance(res[0], synapse.api.errors.Codes)
|
||||
and isinstance(res[1], dict)
|
||||
):
|
||||
return res
|
||||
else:
|
||||
logger.warning(
|
||||
"Module returned invalid value, rejecting room create as spam"
|
||||
)
|
||||
return synapse.api.errors.Codes.FORBIDDEN, {}
|
||||
return synapse.api.errors.Codes.FORBIDDEN
|
||||
|
||||
return self.NOT_SPAM
|
||||
|
||||
async def user_may_publish_room(
|
||||
self, userid: str, room_id: str
|
||||
) -> Union[Tuple[Codes, dict], Literal["NOT_SPAM"]]:
|
||||
) -> Union["synapse.api.errors.Codes", Literal["NOT_SPAM"]]:
|
||||
"""Checks if a given user may publish a room to the directory
|
||||
|
||||
Args:
|
||||
@@ -682,21 +603,14 @@ class SpamChecker:
|
||||
if res is True or res is self.NOT_SPAM:
|
||||
continue
|
||||
elif res is False:
|
||||
return synapse.api.errors.Codes.FORBIDDEN, {}
|
||||
return synapse.api.errors.Codes.FORBIDDEN
|
||||
elif isinstance(res, synapse.api.errors.Codes):
|
||||
return res, {}
|
||||
elif (
|
||||
isinstance(res, tuple)
|
||||
and len(res) == 2
|
||||
and isinstance(res[0], synapse.api.errors.Codes)
|
||||
and isinstance(res[1], dict)
|
||||
):
|
||||
return res
|
||||
else:
|
||||
logger.warning(
|
||||
"Module returned invalid value, rejecting room publication as spam"
|
||||
)
|
||||
return synapse.api.errors.Codes.FORBIDDEN, {}
|
||||
return synapse.api.errors.Codes.FORBIDDEN
|
||||
|
||||
return self.NOT_SPAM
|
||||
|
||||
@@ -764,7 +678,7 @@ class SpamChecker:
|
||||
|
||||
async def check_media_file_for_spam(
|
||||
self, file_wrapper: ReadableFileWrapper, file_info: FileInfo
|
||||
) -> Union[Tuple[Codes, dict], Literal["NOT_SPAM"]]:
|
||||
) -> Union["synapse.api.errors.Codes", Literal["NOT_SPAM"]]:
|
||||
"""Checks if a piece of newly uploaded media should be blocked.
|
||||
|
||||
This will be called for local uploads, downloads of remote media, each
|
||||
@@ -801,20 +715,13 @@ class SpamChecker:
|
||||
if res is False or res is self.NOT_SPAM:
|
||||
continue
|
||||
elif res is True:
|
||||
return synapse.api.errors.Codes.FORBIDDEN, {}
|
||||
return synapse.api.errors.Codes.FORBIDDEN
|
||||
elif isinstance(res, synapse.api.errors.Codes):
|
||||
return res, {}
|
||||
elif (
|
||||
isinstance(res, tuple)
|
||||
and len(res) == 2
|
||||
and isinstance(res[0], synapse.api.errors.Codes)
|
||||
and isinstance(res[1], dict)
|
||||
):
|
||||
return res
|
||||
else:
|
||||
logger.warning(
|
||||
"Module returned invalid value, rejecting media file as spam"
|
||||
)
|
||||
return synapse.api.errors.Codes.FORBIDDEN, {}
|
||||
return synapse.api.errors.Codes.FORBIDDEN
|
||||
|
||||
return self.NOT_SPAM
|
||||
|
||||
@@ -149,8 +149,7 @@ class DirectoryHandler:
|
||||
raise AuthError(
|
||||
403,
|
||||
"This user is not permitted to create this alias",
|
||||
errcode=spam_check[0],
|
||||
additional_fields=spam_check[1],
|
||||
spam_check,
|
||||
)
|
||||
|
||||
if not self.config.roomdirectory.is_alias_creation_allowed(
|
||||
@@ -442,8 +441,7 @@ class DirectoryHandler:
|
||||
raise AuthError(
|
||||
403,
|
||||
"This user is not permitted to publish rooms to the room list",
|
||||
errcode=spam_check[0],
|
||||
additional_fields=spam_check[1],
|
||||
spam_check,
|
||||
)
|
||||
|
||||
if requester.is_guest:
|
||||
|
||||
@@ -844,8 +844,7 @@ class FederationHandler:
|
||||
raise SynapseError(
|
||||
403,
|
||||
"This user is not permitted to send invites to this server/user",
|
||||
errcode=spam_check[0],
|
||||
additional_fields=spam_check[1],
|
||||
spam_check,
|
||||
)
|
||||
|
||||
membership = event.content.get("membership")
|
||||
|
||||
@@ -440,12 +440,7 @@ class RoomCreationHandler:
|
||||
|
||||
spam_check = await self.spam_checker.user_may_create_room(user_id)
|
||||
if spam_check != NOT_SPAM:
|
||||
raise SynapseError(
|
||||
403,
|
||||
"You are not permitted to create rooms",
|
||||
errcode=spam_check[0],
|
||||
additional_fields=spam_check[1],
|
||||
)
|
||||
raise SynapseError(403, "You are not permitted to create rooms", spam_check)
|
||||
|
||||
creation_content: JsonDict = {
|
||||
"room_version": new_room_version.identifier,
|
||||
@@ -736,10 +731,7 @@ class RoomCreationHandler:
|
||||
spam_check = await self.spam_checker.user_may_create_room(user_id)
|
||||
if spam_check != NOT_SPAM:
|
||||
raise SynapseError(
|
||||
403,
|
||||
"You are not permitted to create rooms",
|
||||
errcode=spam_check[0],
|
||||
additional_fields=spam_check[1],
|
||||
403, "You are not permitted to create rooms", spam_check
|
||||
)
|
||||
|
||||
if ratelimit:
|
||||
@@ -1019,8 +1011,6 @@ class RoomCreationHandler:
|
||||
|
||||
event_keys = {"room_id": room_id, "sender": creator_id, "state_key": ""}
|
||||
|
||||
last_sent_event_id: Optional[str] = None
|
||||
|
||||
def create(etype: str, content: JsonDict, **kwargs: Any) -> JsonDict:
|
||||
e = {"type": etype, "content": content}
|
||||
|
||||
@@ -1030,27 +1020,19 @@ class RoomCreationHandler:
|
||||
return e
|
||||
|
||||
async def send(etype: str, content: JsonDict, **kwargs: Any) -> int:
|
||||
nonlocal last_sent_event_id
|
||||
|
||||
event = create(etype, content, **kwargs)
|
||||
logger.debug("Sending %s in new room", etype)
|
||||
# Allow these events to be sent even if the user is shadow-banned to
|
||||
# allow the room creation to complete.
|
||||
(
|
||||
sent_event,
|
||||
_,
|
||||
last_stream_id,
|
||||
) = await self.event_creation_handler.create_and_send_nonmember_event(
|
||||
creator,
|
||||
event,
|
||||
ratelimit=False,
|
||||
ignore_shadow_ban=True,
|
||||
# Note: we don't pass state_event_ids here because this triggers
|
||||
# an additional query per event to look them up from the events table.
|
||||
prev_event_ids=[last_sent_event_id] if last_sent_event_id else [],
|
||||
)
|
||||
|
||||
last_sent_event_id = sent_event.event_id
|
||||
|
||||
return last_stream_id
|
||||
|
||||
try:
|
||||
@@ -1064,9 +1046,7 @@ class RoomCreationHandler:
|
||||
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(
|
||||
await self.room_member_handler.update_membership(
|
||||
creator,
|
||||
creator.user,
|
||||
room_id,
|
||||
@@ -1074,9 +1054,7 @@ class RoomCreationHandler:
|
||||
ratelimit=ratelimit,
|
||||
content=creator_join_profile,
|
||||
new_room=True,
|
||||
prev_event_ids=[last_sent_event_id],
|
||||
)
|
||||
last_sent_event_id = member_event_id
|
||||
|
||||
# We treat the power levels override specially as this needs to be one
|
||||
# of the first events that get sent into a room.
|
||||
@@ -1397,7 +1375,6 @@ class TimestampLookupHandler:
|
||||
# the timestamp given and the event we were able to find locally
|
||||
is_event_next_to_backward_gap = False
|
||||
is_event_next_to_forward_gap = False
|
||||
local_event = None
|
||||
if local_event_id:
|
||||
local_event = await self.store.get_event(
|
||||
local_event_id, allow_none=False, allow_rejected=False
|
||||
@@ -1484,10 +1461,7 @@ class TimestampLookupHandler:
|
||||
ex.args,
|
||||
)
|
||||
|
||||
# To appease mypy, we have to add both of these conditions to check for
|
||||
# `None`. We only expect `local_event` to be `None` when
|
||||
# `local_event_id` is `None` but mypy isn't as smart and assuming as us.
|
||||
if not local_event_id or not local_event:
|
||||
if not local_event_id:
|
||||
raise SynapseError(
|
||||
404,
|
||||
"Unable to find event from %s in direction %s" % (timestamp, direction),
|
||||
|
||||
@@ -685,7 +685,7 @@ class RoomMemberHandler(metaclass=abc.ABCMeta):
|
||||
if target_id == self._server_notices_mxid:
|
||||
raise SynapseError(HTTPStatus.FORBIDDEN, "Cannot invite this user")
|
||||
|
||||
block_invite_result = None
|
||||
block_invite_code = None
|
||||
|
||||
if (
|
||||
self._server_notices_mxid is not None
|
||||
@@ -703,21 +703,18 @@ class RoomMemberHandler(metaclass=abc.ABCMeta):
|
||||
"Blocking invite: user is not admin and non-admin "
|
||||
"invites disabled"
|
||||
)
|
||||
block_invite_result = (Codes.FORBIDDEN, {})
|
||||
block_invite_code = Codes.FORBIDDEN
|
||||
|
||||
spam_check = await self.spam_checker.user_may_invite(
|
||||
requester.user.to_string(), target_id, room_id
|
||||
)
|
||||
if spam_check != NOT_SPAM:
|
||||
logger.info("Blocking invite due to spam checker")
|
||||
block_invite_result = spam_check
|
||||
block_invite_code = spam_check
|
||||
|
||||
if block_invite_result is not None:
|
||||
if block_invite_code is not None:
|
||||
raise SynapseError(
|
||||
403,
|
||||
"Invites have been disabled on this server",
|
||||
errcode=block_invite_result[0],
|
||||
additional_fields=block_invite_result[1],
|
||||
403, "Invites have been disabled on this server", block_invite_code
|
||||
)
|
||||
|
||||
# An empty prev_events list is allowed as long as the auth_event_ids are present
|
||||
@@ -831,12 +828,7 @@ class RoomMemberHandler(metaclass=abc.ABCMeta):
|
||||
target.to_string(), room_id, is_invited=inviter is not None
|
||||
)
|
||||
if spam_check != NOT_SPAM:
|
||||
raise SynapseError(
|
||||
403,
|
||||
"Not allowed to join this room",
|
||||
errcode=spam_check[0],
|
||||
additional_fields=spam_check[1],
|
||||
)
|
||||
raise SynapseError(403, "Not allowed to join this room", spam_check)
|
||||
|
||||
# Check if a remote join should be performed.
|
||||
remote_join, remote_room_hosts = await self._should_perform_remote_join(
|
||||
@@ -1395,12 +1387,7 @@ class RoomMemberHandler(metaclass=abc.ABCMeta):
|
||||
room_id=room_id,
|
||||
)
|
||||
if spam_check != NOT_SPAM:
|
||||
raise SynapseError(
|
||||
403,
|
||||
"Cannot send threepid invite",
|
||||
errcode=spam_check[0],
|
||||
additional_fields=spam_check[1],
|
||||
)
|
||||
raise SynapseError(403, "Cannot send threepid invite", spam_check)
|
||||
|
||||
stream_id = await self._make_and_store_3pid_invite(
|
||||
requester,
|
||||
|
||||
@@ -35,7 +35,6 @@ from typing_extensions import ParamSpec
|
||||
from twisted.internet import defer
|
||||
from twisted.web.resource import Resource
|
||||
|
||||
from synapse.api import errors
|
||||
from synapse.api.errors import SynapseError
|
||||
from synapse.events import EventBase
|
||||
from synapse.events.presence_router import (
|
||||
|
||||
@@ -17,6 +17,7 @@ import itertools
|
||||
import logging
|
||||
from typing import TYPE_CHECKING, Any, Dict, Iterable, List, Optional, Set, Tuple, Union
|
||||
|
||||
import attr
|
||||
from prometheus_client import Counter
|
||||
|
||||
from synapse.api.constants import EventTypes, Membership, RelationTypes
|
||||
@@ -25,11 +26,13 @@ from synapse.events import EventBase, relation_from_event
|
||||
from synapse.events.snapshot import EventContext
|
||||
from synapse.state import POWER_KEY
|
||||
from synapse.storage.databases.main.roommember import EventIdMembership
|
||||
from synapse.storage.state import StateFilter
|
||||
from synapse.util.caches import register_cache
|
||||
from synapse.util.async_helpers import Linearizer
|
||||
from synapse.util.caches import CacheMetric, register_cache
|
||||
from synapse.util.caches.descriptors import lru_cache
|
||||
from synapse.util.caches.lrucache import LruCache
|
||||
from synapse.util.metrics import measure_func
|
||||
from synapse.visibility import filter_event_for_clients_with_state
|
||||
|
||||
from ..storage.state import StateFilter
|
||||
from .push_rule_evaluator import PushRuleEvaluatorForEvent
|
||||
|
||||
if TYPE_CHECKING:
|
||||
@@ -45,6 +48,15 @@ push_rules_state_size_counter = Counter(
|
||||
"synapse_push_bulk_push_rule_evaluator_push_rules_state_size_counter", ""
|
||||
)
|
||||
|
||||
# Measures whether we use the fast path of using state deltas, or if we have to
|
||||
# recalculate from scratch
|
||||
push_rules_delta_state_cache_metric = register_cache(
|
||||
"cache",
|
||||
"push_rules_delta_state_cache_metric",
|
||||
cache=[], # Meaningless size, as this isn't a cache that stores values
|
||||
resizable=False,
|
||||
)
|
||||
|
||||
|
||||
STATE_EVENT_TYPES_TO_MARK_UNREAD = {
|
||||
EventTypes.Topic,
|
||||
@@ -99,6 +111,10 @@ class BulkPushRuleEvaluator:
|
||||
self.clock = hs.get_clock()
|
||||
self._event_auth_handler = hs.get_event_auth_handler()
|
||||
|
||||
# Used by `RulesForRoom` to ensure only one thing mutates the cache at a
|
||||
# time. Keyed off room_id.
|
||||
self._rules_linearizer = Linearizer(name="rules_for_room")
|
||||
|
||||
self.room_push_rule_cache_metrics = register_cache(
|
||||
"cache",
|
||||
"room_push_rule_cache",
|
||||
@@ -110,55 +126,48 @@ class BulkPushRuleEvaluator:
|
||||
self._relations_match_enabled = self.hs.config.experimental.msc3772_enabled
|
||||
|
||||
async def _get_rules_for_event(
|
||||
self,
|
||||
event: EventBase,
|
||||
self, event: EventBase, context: EventContext
|
||||
) -> Dict[str, List[Dict[str, Any]]]:
|
||||
"""Get the push rules for all users who may need to be notified about
|
||||
the event.
|
||||
|
||||
Note: this does not check if the user is allowed to see the event.
|
||||
"""This gets the rules for all users in the room at the time of the event,
|
||||
as well as the push rules for the invitee if the event is an invite.
|
||||
|
||||
Returns:
|
||||
Mapping of user ID to their push rules.
|
||||
dict of user_id -> push_rules
|
||||
"""
|
||||
# We get the users who may need to be notified by first fetching the
|
||||
# local users currently in the room, finding those that have push rules,
|
||||
# and *then* checking which users are actually allowed to see the event.
|
||||
#
|
||||
# The alternative is to first fetch all users that were joined at the
|
||||
# event, but that requires fetching the full state at the event, which
|
||||
# may be expensive for large rooms with few local users.
|
||||
room_id = event.room_id
|
||||
|
||||
local_users = await self.store.get_local_users_in_room(event.room_id)
|
||||
rules_for_room_data = self._get_rules_for_room(room_id)
|
||||
rules_for_room = RulesForRoom(
|
||||
hs=self.hs,
|
||||
room_id=room_id,
|
||||
rules_for_room_cache=self._get_rules_for_room.cache,
|
||||
room_push_rule_cache_metrics=self.room_push_rule_cache_metrics,
|
||||
linearizer=self._rules_linearizer,
|
||||
cached_data=rules_for_room_data,
|
||||
)
|
||||
|
||||
# Filter out appservice users.
|
||||
local_users = [
|
||||
u
|
||||
for u in local_users
|
||||
if not self.store.get_if_app_services_interested_in_user(u)
|
||||
]
|
||||
rules_by_user = await rules_for_room.get_rules(event, context)
|
||||
|
||||
# if this event is an invite event, we may need to run rules for the user
|
||||
# who's been invited, otherwise they won't get told they've been invited
|
||||
if event.type == EventTypes.Member and event.membership == Membership.INVITE:
|
||||
if event.type == "m.room.member" and event.content["membership"] == "invite":
|
||||
invited = event.state_key
|
||||
if invited and self.hs.is_mine_id(invited) and invited not in local_users:
|
||||
local_users = list(local_users)
|
||||
local_users.append(invited)
|
||||
|
||||
rules_by_user = await self.store.bulk_get_push_rules(local_users)
|
||||
|
||||
logger.debug("Users in room: %s", local_users)
|
||||
|
||||
if logger.isEnabledFor(logging.DEBUG):
|
||||
logger.debug(
|
||||
"Returning push rules for %r %r",
|
||||
event.room_id,
|
||||
list(rules_by_user.keys()),
|
||||
)
|
||||
if invited and self.hs.is_mine_id(invited):
|
||||
rules_by_user = dict(rules_by_user)
|
||||
rules_by_user[invited] = await self.store.get_push_rules_for_user(
|
||||
invited
|
||||
)
|
||||
|
||||
return rules_by_user
|
||||
|
||||
@lru_cache()
|
||||
def _get_rules_for_room(self, room_id: str) -> "RulesForRoomData":
|
||||
"""Get the current RulesForRoomData object for the given room id"""
|
||||
# It's important that the RulesForRoomData object gets added to self._get_rules_for_room.cache
|
||||
# before any lookup methods get called on it as otherwise there may be
|
||||
# a race if invalidate_all gets called (which assumes its in the cache)
|
||||
return RulesForRoomData()
|
||||
|
||||
async def _get_power_levels_and_sender_level(
|
||||
self, event: EventBase, context: EventContext
|
||||
) -> Tuple[dict, int]:
|
||||
@@ -253,12 +262,10 @@ class BulkPushRuleEvaluator:
|
||||
|
||||
count_as_unread = _should_count_as_unread(event, context)
|
||||
|
||||
rules_by_user = await self._get_rules_for_event(event)
|
||||
rules_by_user = await self._get_rules_for_event(event, context)
|
||||
actions_by_user: Dict[str, List[Union[dict, str]]] = {}
|
||||
|
||||
room_member_count = await self.store.get_number_joined_users_in_room(
|
||||
event.room_id
|
||||
)
|
||||
room_members = await self.store.get_joined_users_from_context(event, context)
|
||||
|
||||
(
|
||||
power_levels,
|
||||
@@ -271,36 +278,30 @@ class BulkPushRuleEvaluator:
|
||||
|
||||
evaluator = PushRuleEvaluatorForEvent(
|
||||
event,
|
||||
room_member_count,
|
||||
len(room_members),
|
||||
sender_power_level,
|
||||
power_levels,
|
||||
relations,
|
||||
self._relations_match_enabled,
|
||||
)
|
||||
|
||||
users = rules_by_user.keys()
|
||||
profiles = await self.store.get_subset_users_in_room_with_profiles(
|
||||
event.room_id, users
|
||||
)
|
||||
|
||||
# This is a check for the case where user joins a room without being
|
||||
# allowed to see history, and then the server receives a delayed event
|
||||
# from before the user joined, which they should not be pushed for
|
||||
uids_with_visibility = await filter_event_for_clients_with_state(
|
||||
self.store, users, event, context
|
||||
)
|
||||
# If the event is not a state event check if any users ignore the sender.
|
||||
if not event.is_state():
|
||||
ignorers = await self.store.ignored_by(event.sender)
|
||||
else:
|
||||
ignorers = frozenset()
|
||||
|
||||
for uid, rules in rules_by_user.items():
|
||||
if event.sender == uid:
|
||||
continue
|
||||
|
||||
if uid not in uids_with_visibility:
|
||||
if uid in ignorers:
|
||||
continue
|
||||
|
||||
display_name = None
|
||||
profile = profiles.get(uid)
|
||||
if profile:
|
||||
display_name = profile.display_name
|
||||
profile_info = room_members.get(uid)
|
||||
if profile_info:
|
||||
display_name = profile_info.display_name
|
||||
|
||||
if not display_name:
|
||||
# Handle the case where we are pushing a membership event to
|
||||
@@ -345,3 +346,283 @@ MemberMap = Dict[str, Optional[EventIdMembership]]
|
||||
Rule = Dict[str, dict]
|
||||
RulesByUser = Dict[str, List[Rule]]
|
||||
StateGroup = Union[object, int]
|
||||
|
||||
|
||||
@attr.s(slots=True, auto_attribs=True)
|
||||
class RulesForRoomData:
|
||||
"""The data stored in the cache by `RulesForRoom`.
|
||||
|
||||
We don't store `RulesForRoom` directly in the cache as we want our caches to
|
||||
*only* include data, and not references to e.g. the data stores.
|
||||
"""
|
||||
|
||||
# event_id -> EventIdMembership
|
||||
member_map: MemberMap = attr.Factory(dict)
|
||||
# user_id -> rules
|
||||
rules_by_user: RulesByUser = attr.Factory(dict)
|
||||
|
||||
# The last state group we updated the caches for. If the state_group of
|
||||
# a new event comes along, we know that we can just return the cached
|
||||
# result.
|
||||
# On invalidation of the rules themselves (if the user changes them),
|
||||
# we invalidate everything and set state_group to `object()`
|
||||
state_group: StateGroup = attr.Factory(object)
|
||||
|
||||
# A sequence number to keep track of when we're allowed to update the
|
||||
# cache. We bump the sequence number when we invalidate the cache. If
|
||||
# the sequence number changes while we're calculating stuff we should
|
||||
# not update the cache with it.
|
||||
sequence: int = 0
|
||||
|
||||
# A cache of user_ids that we *know* aren't interesting, e.g. user_ids
|
||||
# owned by AS's, or remote users, etc. (I.e. users we will never need to
|
||||
# calculate push for)
|
||||
# These never need to be invalidated as we will never set up push for
|
||||
# them.
|
||||
uninteresting_user_set: Set[str] = attr.Factory(set)
|
||||
|
||||
|
||||
class RulesForRoom:
|
||||
"""Caches push rules for users in a room.
|
||||
|
||||
This efficiently handles users joining/leaving the room by not invalidating
|
||||
the entire cache for the room.
|
||||
|
||||
A new instance is constructed for each call to
|
||||
`BulkPushRuleEvaluator._get_rules_for_event`, with the cached data from
|
||||
previous calls passed in.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
hs: "HomeServer",
|
||||
room_id: str,
|
||||
rules_for_room_cache: LruCache,
|
||||
room_push_rule_cache_metrics: CacheMetric,
|
||||
linearizer: Linearizer,
|
||||
cached_data: RulesForRoomData,
|
||||
):
|
||||
"""
|
||||
Args:
|
||||
hs: The HomeServer object.
|
||||
room_id: The room ID.
|
||||
rules_for_room_cache: The cache object that caches these
|
||||
RoomsForUser objects.
|
||||
room_push_rule_cache_metrics: The metrics object
|
||||
linearizer: The linearizer used to ensure only one thing mutates
|
||||
the cache at a time. Keyed off room_id
|
||||
cached_data: Cached data from previous calls to `self.get_rules`,
|
||||
can be mutated.
|
||||
"""
|
||||
self.room_id = room_id
|
||||
self.is_mine_id = hs.is_mine_id
|
||||
self.store = hs.get_datastores().main
|
||||
self.room_push_rule_cache_metrics = room_push_rule_cache_metrics
|
||||
|
||||
# Used to ensure only one thing mutates the cache at a time. Keyed off
|
||||
# room_id.
|
||||
self.linearizer = linearizer
|
||||
|
||||
self.data = cached_data
|
||||
|
||||
# We need to be clever on the invalidating caches callbacks, as
|
||||
# otherwise the invalidation callback holds a reference to the object,
|
||||
# potentially causing it to leak.
|
||||
# To get around this we pass a function that on invalidations looks ups
|
||||
# the RoomsForUser entry in the cache, rather than keeping a reference
|
||||
# to self around in the callback.
|
||||
self.invalidate_all_cb = _Invalidation(rules_for_room_cache, room_id)
|
||||
|
||||
async def get_rules(
|
||||
self, event: EventBase, context: EventContext
|
||||
) -> Dict[str, List[Dict[str, dict]]]:
|
||||
"""Given an event context return the rules for all users who are
|
||||
currently in the room.
|
||||
"""
|
||||
state_group = context.state_group
|
||||
|
||||
if state_group and self.data.state_group == state_group:
|
||||
logger.debug("Using cached rules for %r", self.room_id)
|
||||
self.room_push_rule_cache_metrics.inc_hits()
|
||||
return self.data.rules_by_user
|
||||
|
||||
async with self.linearizer.queue(self.room_id):
|
||||
if state_group and self.data.state_group == state_group:
|
||||
logger.debug("Using cached rules for %r", self.room_id)
|
||||
self.room_push_rule_cache_metrics.inc_hits()
|
||||
return self.data.rules_by_user
|
||||
|
||||
self.room_push_rule_cache_metrics.inc_misses()
|
||||
|
||||
ret_rules_by_user = {}
|
||||
missing_member_event_ids = {}
|
||||
if state_group and self.data.state_group == context.prev_group:
|
||||
# If we have a simple delta then we can reuse most of the previous
|
||||
# results.
|
||||
ret_rules_by_user = self.data.rules_by_user
|
||||
current_state_ids = context.delta_ids
|
||||
|
||||
push_rules_delta_state_cache_metric.inc_hits()
|
||||
else:
|
||||
current_state_ids = await context.get_current_state_ids()
|
||||
push_rules_delta_state_cache_metric.inc_misses()
|
||||
# Ensure the state IDs exist.
|
||||
assert current_state_ids is not None
|
||||
|
||||
push_rules_state_size_counter.inc(len(current_state_ids))
|
||||
|
||||
logger.debug(
|
||||
"Looking for member changes in %r %r", state_group, current_state_ids
|
||||
)
|
||||
|
||||
# Loop through to see which member events we've seen and have rules
|
||||
# for and which we need to fetch
|
||||
for key in current_state_ids:
|
||||
typ, user_id = key
|
||||
if typ != EventTypes.Member:
|
||||
continue
|
||||
|
||||
if user_id in self.data.uninteresting_user_set:
|
||||
continue
|
||||
|
||||
if not self.is_mine_id(user_id):
|
||||
self.data.uninteresting_user_set.add(user_id)
|
||||
continue
|
||||
|
||||
if self.store.get_if_app_services_interested_in_user(user_id):
|
||||
self.data.uninteresting_user_set.add(user_id)
|
||||
continue
|
||||
|
||||
event_id = current_state_ids[key]
|
||||
|
||||
res = self.data.member_map.get(event_id, None)
|
||||
if res:
|
||||
if res.membership == Membership.JOIN:
|
||||
rules = self.data.rules_by_user.get(res.user_id, None)
|
||||
if rules:
|
||||
ret_rules_by_user[res.user_id] = rules
|
||||
continue
|
||||
|
||||
# If a user has left a room we remove their push rule. If they
|
||||
# joined then we re-add it later in _update_rules_with_member_event_ids
|
||||
ret_rules_by_user.pop(user_id, None)
|
||||
missing_member_event_ids[user_id] = event_id
|
||||
|
||||
if missing_member_event_ids:
|
||||
# If we have some member events we haven't seen, look them up
|
||||
# and fetch push rules for them if appropriate.
|
||||
logger.debug("Found new member events %r", missing_member_event_ids)
|
||||
await self._update_rules_with_member_event_ids(
|
||||
ret_rules_by_user, missing_member_event_ids, state_group, event
|
||||
)
|
||||
else:
|
||||
# The push rules didn't change but lets update the cache anyway
|
||||
self.update_cache(
|
||||
self.data.sequence,
|
||||
members={}, # There were no membership changes
|
||||
rules_by_user=ret_rules_by_user,
|
||||
state_group=state_group,
|
||||
)
|
||||
|
||||
if logger.isEnabledFor(logging.DEBUG):
|
||||
logger.debug(
|
||||
"Returning push rules for %r %r", self.room_id, ret_rules_by_user.keys()
|
||||
)
|
||||
return ret_rules_by_user
|
||||
|
||||
async def _update_rules_with_member_event_ids(
|
||||
self,
|
||||
ret_rules_by_user: Dict[str, list],
|
||||
member_event_ids: Dict[str, str],
|
||||
state_group: Optional[int],
|
||||
event: EventBase,
|
||||
) -> None:
|
||||
"""Update the partially filled rules_by_user dict by fetching rules for
|
||||
any newly joined users in the `member_event_ids` list.
|
||||
|
||||
Args:
|
||||
ret_rules_by_user: Partially filled dict of push rules. Gets
|
||||
updated with any new rules.
|
||||
member_event_ids: Dict of user id to event id for membership events
|
||||
that have happened since the last time we filled rules_by_user
|
||||
state_group: The state group we are currently computing push rules
|
||||
for. Used when updating the cache.
|
||||
event: The event we are currently computing push rules for.
|
||||
"""
|
||||
sequence = self.data.sequence
|
||||
|
||||
members = await self.store.get_membership_from_event_ids(
|
||||
member_event_ids.values()
|
||||
)
|
||||
|
||||
# If the event is a join event then it will be in current state events
|
||||
# map but not in the DB, so we have to explicitly insert it.
|
||||
if event.type == EventTypes.Member:
|
||||
for event_id in member_event_ids.values():
|
||||
if event_id == event.event_id:
|
||||
members[event_id] = EventIdMembership(
|
||||
user_id=event.state_key, membership=event.membership
|
||||
)
|
||||
|
||||
if logger.isEnabledFor(logging.DEBUG):
|
||||
logger.debug("Found members %r: %r", self.room_id, members.values())
|
||||
|
||||
joined_user_ids = {
|
||||
entry.user_id
|
||||
for entry in members.values()
|
||||
if entry and entry.membership == Membership.JOIN
|
||||
}
|
||||
|
||||
logger.debug("Joined: %r", joined_user_ids)
|
||||
|
||||
# Previously we only considered users with pushers or read receipts in that
|
||||
# room. We can't do this anymore because we use push actions to calculate unread
|
||||
# counts, which don't rely on the user having pushers or sent a read receipt into
|
||||
# the room. Therefore we just need to filter for local users here.
|
||||
user_ids = list(filter(self.is_mine_id, joined_user_ids))
|
||||
|
||||
rules_by_user = await self.store.bulk_get_push_rules(
|
||||
user_ids, on_invalidate=self.invalidate_all_cb
|
||||
)
|
||||
|
||||
ret_rules_by_user.update(
|
||||
item for item in rules_by_user.items() if item[0] is not None
|
||||
)
|
||||
|
||||
self.update_cache(sequence, members, ret_rules_by_user, state_group)
|
||||
|
||||
def update_cache(
|
||||
self,
|
||||
sequence: int,
|
||||
members: MemberMap,
|
||||
rules_by_user: RulesByUser,
|
||||
state_group: StateGroup,
|
||||
) -> None:
|
||||
if sequence == self.data.sequence:
|
||||
self.data.member_map.update(members)
|
||||
self.data.rules_by_user = rules_by_user
|
||||
self.data.state_group = state_group
|
||||
|
||||
|
||||
@attr.attrs(slots=True, frozen=True, auto_attribs=True)
|
||||
class _Invalidation:
|
||||
# _Invalidation is passed as an `on_invalidate` callback to bulk_get_push_rules,
|
||||
# which means that it it is stored on the bulk_get_push_rules cache entry. In order
|
||||
# to ensure that we don't accumulate lots of redundant callbacks on the cache entry,
|
||||
# we need to ensure that two _Invalidation objects are "equal" if they refer to the
|
||||
# same `cache` and `room_id`.
|
||||
#
|
||||
# attrs provides suitable __hash__ and __eq__ methods, provided we remember to
|
||||
# set `frozen=True`.
|
||||
|
||||
cache: LruCache
|
||||
room_id: str
|
||||
|
||||
def __call__(self) -> None:
|
||||
rules_data = self.cache.get(self.room_id, None, update_metrics=False)
|
||||
if rules_data:
|
||||
rules_data.sequence += 1
|
||||
rules_data.state_group = object()
|
||||
rules_data.member_map = {}
|
||||
rules_data.rules_by_user = {}
|
||||
push_rules_invalidation_counter.inc()
|
||||
|
||||
@@ -154,9 +154,7 @@ class MediaStorage:
|
||||
# Note that we'll delete the stored media, due to the
|
||||
# try/except below. The media also won't be stored in
|
||||
# the DB.
|
||||
# We currently ignore any additional field returned by
|
||||
# the spam-check API.
|
||||
raise SpamMediaException(errcode=spam_check[0])
|
||||
raise SpamMediaException(errcode=spam_check)
|
||||
|
||||
for provider in self.storage_providers:
|
||||
await provider.store_file(path, file_info)
|
||||
|
||||
@@ -75,15 +75,6 @@ class SQLBaseStore(metaclass=ABCMeta):
|
||||
self._attempt_to_invalidate_cache(
|
||||
"get_users_in_room_with_profiles", (room_id,)
|
||||
)
|
||||
self._attempt_to_invalidate_cache(
|
||||
"get_number_joined_users_in_room", (room_id,)
|
||||
)
|
||||
self._attempt_to_invalidate_cache("get_local_users_in_room", (room_id,))
|
||||
|
||||
for user_id in members_changed:
|
||||
self._attempt_to_invalidate_cache(
|
||||
"get_user_in_room_with_profile", (room_id, user_id)
|
||||
)
|
||||
|
||||
# Purge other caches based on room state.
|
||||
self._attempt_to_invalidate_cache("get_room_summary", (room_id,))
|
||||
|
||||
@@ -143,6 +143,7 @@ class EventPushActionsWorkerStore(ReceiptsWorkerStore, StreamWorkerStore, SQLBas
|
||||
self._find_stream_orderings_for_times, 10 * 60 * 1000
|
||||
)
|
||||
|
||||
self._rotate_delay = 3
|
||||
self._rotate_count = 10000
|
||||
self._doing_notif_rotation = False
|
||||
if hs.config.worker.run_background_tasks:
|
||||
@@ -846,6 +847,7 @@ class EventPushActionsWorkerStore(ReceiptsWorkerStore, StreamWorkerStore, SQLBas
|
||||
)
|
||||
if caught_up:
|
||||
break
|
||||
await self.hs.get_clock().sleep(self._rotate_delay)
|
||||
|
||||
# Finally we clear out old event push actions.
|
||||
await self._remove_old_push_actions_that_have_rotated()
|
||||
@@ -1014,14 +1016,9 @@ class EventPushActionsWorkerStore(ReceiptsWorkerStore, StreamWorkerStore, SQLBas
|
||||
upd.stream_ordering
|
||||
FROM (
|
||||
SELECT user_id, room_id, count(*) as cnt,
|
||||
max(ea.stream_ordering) as stream_ordering
|
||||
FROM event_push_actions AS ea
|
||||
LEFT JOIN event_push_summary AS old USING (user_id, room_id)
|
||||
WHERE ? < ea.stream_ordering AND ea.stream_ordering <= ?
|
||||
AND (
|
||||
old.last_receipt_stream_ordering IS NULL
|
||||
OR old.last_receipt_stream_ordering < ea.stream_ordering
|
||||
)
|
||||
max(stream_ordering) as stream_ordering
|
||||
FROM event_push_actions
|
||||
WHERE ? < stream_ordering AND stream_ordering <= ?
|
||||
AND %s = 1
|
||||
GROUP BY user_id, room_id
|
||||
) AS upd
|
||||
@@ -1112,7 +1109,7 @@ class EventPushActionsWorkerStore(ReceiptsWorkerStore, StreamWorkerStore, SQLBas
|
||||
) -> bool:
|
||||
# We don't want to clear out too much at a time, so we bound our
|
||||
# deletes.
|
||||
batch_size = self._rotate_count
|
||||
batch_size = 10000
|
||||
|
||||
txn.execute(
|
||||
"""
|
||||
|
||||
@@ -1797,18 +1797,6 @@ class PersistEventsStore:
|
||||
self.store.get_invited_rooms_for_local_user.invalidate,
|
||||
(event.state_key,),
|
||||
)
|
||||
txn.call_after(
|
||||
self.store.get_local_users_in_room.invalidate,
|
||||
(event.room_id,),
|
||||
)
|
||||
txn.call_after(
|
||||
self.store.get_number_joined_users_in_room.invalidate,
|
||||
(event.room_id,),
|
||||
)
|
||||
txn.call_after(
|
||||
self.store.get_user_in_room_with_profile.invalidate,
|
||||
(event.room_id, event.state_key),
|
||||
)
|
||||
|
||||
# The `_get_membership_from_event_id` is immutable, except for the
|
||||
# case where we look up an event *before* persisting it.
|
||||
|
||||
@@ -212,60 +212,6 @@ class RoomMemberWorkerStore(EventsWorkerStore):
|
||||
txn.execute(sql, (room_id, Membership.JOIN))
|
||||
return [r[0] for r in txn]
|
||||
|
||||
@cached()
|
||||
def get_user_in_room_with_profile(
|
||||
self, room_id: str, user_id: str
|
||||
) -> Dict[str, ProfileInfo]:
|
||||
raise NotImplementedError()
|
||||
|
||||
@cachedList(
|
||||
cached_method_name="get_user_in_room_with_profile", list_name="user_ids"
|
||||
)
|
||||
async def get_subset_users_in_room_with_profiles(
|
||||
self, room_id: str, user_ids: Collection[str]
|
||||
) -> Dict[str, ProfileInfo]:
|
||||
"""Get a mapping from user ID to profile information for a list of users
|
||||
in a given room.
|
||||
|
||||
The profile information comes directly from this room's `m.room.member`
|
||||
events, and so may be specific to this room rather than part of a user's
|
||||
global profile. To avoid privacy leaks, the profile data should only be
|
||||
revealed to users who are already in this room.
|
||||
|
||||
Args:
|
||||
room_id: The ID of the room to retrieve the users of.
|
||||
user_ids: a list of users in the room to run the query for
|
||||
|
||||
Returns:
|
||||
A mapping from user ID to ProfileInfo.
|
||||
"""
|
||||
|
||||
def _get_subset_users_in_room_with_profiles(
|
||||
txn: LoggingTransaction,
|
||||
) -> Dict[str, ProfileInfo]:
|
||||
clause, ids = make_in_list_sql_clause(
|
||||
self.database_engine, "m.user_id", user_ids
|
||||
)
|
||||
|
||||
sql = """
|
||||
SELECT state_key, display_name, avatar_url FROM room_memberships as m
|
||||
INNER JOIN current_state_events as c
|
||||
ON m.event_id = c.event_id
|
||||
AND m.room_id = c.room_id
|
||||
AND m.user_id = c.state_key
|
||||
WHERE c.type = 'm.room.member' AND c.room_id = ? AND m.membership = ? AND %s
|
||||
""" % (
|
||||
clause,
|
||||
)
|
||||
txn.execute(sql, (room_id, Membership.JOIN, *ids))
|
||||
|
||||
return {r[0]: ProfileInfo(display_name=r[1], avatar_url=r[2]) for r in txn}
|
||||
|
||||
return await self.db_pool.runInteraction(
|
||||
"get_subset_users_in_room_with_profiles",
|
||||
_get_subset_users_in_room_with_profiles,
|
||||
)
|
||||
|
||||
@cached(max_entries=100000, iterable=True)
|
||||
async def get_users_in_room_with_profiles(
|
||||
self, room_id: str
|
||||
@@ -391,15 +337,6 @@ class RoomMemberWorkerStore(EventsWorkerStore):
|
||||
"get_room_summary", _get_room_summary_txn
|
||||
)
|
||||
|
||||
@cached()
|
||||
async def get_number_joined_users_in_room(self, room_id: str) -> int:
|
||||
return await self.db_pool.simple_select_one_onecol(
|
||||
table="current_state_events",
|
||||
keyvalues={"room_id": room_id, "membership": Membership.JOIN},
|
||||
retcol="COUNT(*)",
|
||||
desc="get_number_joined_users_in_room",
|
||||
)
|
||||
|
||||
@cached()
|
||||
async def get_invited_rooms_for_local_user(
|
||||
self, user_id: str
|
||||
@@ -479,17 +416,6 @@ class RoomMemberWorkerStore(EventsWorkerStore):
|
||||
user_id: str,
|
||||
membership_list: List[str],
|
||||
) -> List[RoomsForUser]:
|
||||
"""Get all the rooms for this *local* user where the membership for this user
|
||||
matches one in the membership list.
|
||||
|
||||
Args:
|
||||
user_id: The user ID.
|
||||
membership_list: A list of synapse.api.constants.Membership
|
||||
values which the user must be in.
|
||||
|
||||
Returns:
|
||||
The RoomsForUser that the user matches the membership types.
|
||||
"""
|
||||
# Paranoia check.
|
||||
if not self.hs.is_mine_id(user_id):
|
||||
raise Exception(
|
||||
@@ -518,18 +444,6 @@ class RoomMemberWorkerStore(EventsWorkerStore):
|
||||
|
||||
return results
|
||||
|
||||
@cached(iterable=True)
|
||||
async def get_local_users_in_room(self, room_id: str) -> List[str]:
|
||||
"""
|
||||
Retrieves a list of the current roommembers who are local to the server.
|
||||
"""
|
||||
return await self.db_pool.simple_select_onecol(
|
||||
table="local_current_membership",
|
||||
keyvalues={"room_id": room_id, "membership": Membership.JOIN},
|
||||
retcol="user_id",
|
||||
desc="get_local_users_in_room",
|
||||
)
|
||||
|
||||
async def get_local_current_membership_for_user_in_room(
|
||||
self, user_id: str, room_id: str
|
||||
) -> Tuple[Optional[str], Optional[str]]:
|
||||
@@ -562,7 +476,7 @@ class RoomMemberWorkerStore(EventsWorkerStore):
|
||||
|
||||
return results_dict.get("membership"), results_dict.get("event_id")
|
||||
|
||||
@cached(max_entries=500000, iterable=True, prune_unread_entries=False)
|
||||
@cached(max_entries=500000, iterable=True, prune_unread_entries=False, debug_invalidations=True)
|
||||
async def get_rooms_for_user_with_stream_ordering(
|
||||
self, user_id: str
|
||||
) -> FrozenSet[GetRoomsForUserWithStreamOrdering]:
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
# limitations under the License.
|
||||
|
||||
import enum
|
||||
import logging
|
||||
import threading
|
||||
from typing import (
|
||||
Callable,
|
||||
@@ -66,6 +67,7 @@ class DeferredCache(Generic[KT, VT]):
|
||||
"cache",
|
||||
"thread",
|
||||
"_pending_deferred_cache",
|
||||
"debug_invalidations"
|
||||
)
|
||||
|
||||
def __init__(
|
||||
@@ -76,6 +78,7 @@ class DeferredCache(Generic[KT, VT]):
|
||||
iterable: bool = False,
|
||||
apply_cache_factor_from_config: bool = True,
|
||||
prune_unread_entries: bool = True,
|
||||
debug_invalidations: bool = False,
|
||||
):
|
||||
"""
|
||||
Args:
|
||||
@@ -119,6 +122,7 @@ class DeferredCache(Generic[KT, VT]):
|
||||
)
|
||||
|
||||
self.thread: Optional[threading.Thread] = None
|
||||
self.debug_invalidations = debug_invalidations
|
||||
|
||||
@property
|
||||
def max_entries(self) -> int:
|
||||
@@ -310,6 +314,9 @@ class DeferredCache(Generic[KT, VT]):
|
||||
self.check_thread()
|
||||
self.cache.del_multi(key)
|
||||
|
||||
if self.debug_invalidations:
|
||||
logging.debug("Invalidating key %r in cache", key)
|
||||
|
||||
# if we have a pending lookup for this key, remove it from the
|
||||
# _pending_deferred_cache, which will (a) stop it being returned
|
||||
# for future queries and (b) stop it being persisted as a proper entry
|
||||
@@ -326,6 +333,8 @@ class DeferredCache(Generic[KT, VT]):
|
||||
entry.invalidate()
|
||||
|
||||
def invalidate_all(self) -> None:
|
||||
if self.debug_invalidations:
|
||||
logging.debug("Invalidating ALL keys")
|
||||
self.check_thread()
|
||||
self.cache.clear()
|
||||
for entry in self._pending_deferred_cache.values():
|
||||
|
||||
@@ -301,6 +301,7 @@ class DeferredCacheDescriptor(_CacheDescriptorBase):
|
||||
cache_context: bool = False,
|
||||
iterable: bool = False,
|
||||
prune_unread_entries: bool = True,
|
||||
debug_invalidations: bool = False
|
||||
):
|
||||
super().__init__(
|
||||
orig,
|
||||
@@ -318,6 +319,7 @@ class DeferredCacheDescriptor(_CacheDescriptorBase):
|
||||
self.tree = tree
|
||||
self.iterable = iterable
|
||||
self.prune_unread_entries = prune_unread_entries
|
||||
self.debug_invalidations = debug_invalidations
|
||||
|
||||
def __get__(self, obj: Optional[Any], owner: Optional[Type]) -> Callable[..., Any]:
|
||||
cache: DeferredCache[CacheKey, Any] = DeferredCache(
|
||||
@@ -326,6 +328,7 @@ class DeferredCacheDescriptor(_CacheDescriptorBase):
|
||||
tree=self.tree,
|
||||
iterable=self.iterable,
|
||||
prune_unread_entries=self.prune_unread_entries,
|
||||
debug_invalidations=self.debug_invalidations,
|
||||
)
|
||||
|
||||
get_cache_key = self.cache_key_builder
|
||||
@@ -577,6 +580,7 @@ def cached(
|
||||
cache_context: bool = False,
|
||||
iterable: bool = False,
|
||||
prune_unread_entries: bool = True,
|
||||
debug_invalidations: bool = False,
|
||||
) -> Callable[[F], _CachedFunction[F]]:
|
||||
func = lambda orig: DeferredCacheDescriptor(
|
||||
orig,
|
||||
@@ -587,6 +591,7 @@ def cached(
|
||||
cache_context=cache_context,
|
||||
iterable=iterable,
|
||||
prune_unread_entries=prune_unread_entries,
|
||||
debug_invalidations=debug_invalidations
|
||||
)
|
||||
|
||||
return cast(Callable[[F], _CachedFunction[F]], func)
|
||||
|
||||
@@ -13,21 +13,16 @@
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
import logging
|
||||
from enum import Enum, auto
|
||||
from typing import Collection, Dict, FrozenSet, List, Optional, Tuple
|
||||
|
||||
import attr
|
||||
from typing_extensions import Final
|
||||
|
||||
from synapse.api.constants import EventTypes, HistoryVisibility, Membership
|
||||
from synapse.events import EventBase
|
||||
from synapse.events.snapshot import EventContext
|
||||
from synapse.events.utils import prune_event
|
||||
from synapse.storage.controllers import StorageControllers
|
||||
from synapse.storage.databases.main import DataStore
|
||||
from synapse.storage.state import StateFilter
|
||||
from synapse.types import RetentionPolicy, StateMap, get_domain_from_id
|
||||
from synapse.util import Clock
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -107,18 +102,153 @@ async def filter_events_for_client(
|
||||
] = await storage.main.get_retention_policy_for_room(room_id)
|
||||
|
||||
def allowed(event: EventBase) -> Optional[EventBase]:
|
||||
return _check_client_allowed_to_see_event(
|
||||
user_id=user_id,
|
||||
event=event,
|
||||
clock=storage.main.clock,
|
||||
filter_send_to_client=filter_send_to_client,
|
||||
sender_ignored=event.sender in ignore_list,
|
||||
always_include_ids=always_include_ids,
|
||||
retention_policy=retention_policies[room_id],
|
||||
state=event_id_to_state.get(event.event_id),
|
||||
is_peeking=is_peeking,
|
||||
sender_erased=erased_senders.get(event.sender, False),
|
||||
)
|
||||
"""
|
||||
Args:
|
||||
event: event to check
|
||||
|
||||
Returns:
|
||||
None if the user cannot see this event at all
|
||||
|
||||
a redacted copy of the event if they can only see a redacted
|
||||
version
|
||||
|
||||
the original event if they can see it as normal.
|
||||
"""
|
||||
# Only run some checks if these events aren't about to be sent to clients. This is
|
||||
# because, if this is not the case, we're probably only checking if the users can
|
||||
# see events in the room at that point in the DAG, and that shouldn't be decided
|
||||
# on those checks.
|
||||
if filter_send_to_client:
|
||||
if event.type == EventTypes.Dummy:
|
||||
return None
|
||||
|
||||
if not event.is_state() and event.sender in ignore_list:
|
||||
return None
|
||||
|
||||
# Until MSC2261 has landed we can't redact malicious alias events, so for
|
||||
# now we temporarily filter out m.room.aliases entirely to mitigate
|
||||
# abuse, while we spec a better solution to advertising aliases
|
||||
# on rooms.
|
||||
if event.type == EventTypes.Aliases:
|
||||
return None
|
||||
|
||||
# Don't try to apply the room's retention policy if the event is a state
|
||||
# event, as MSC1763 states that retention is only considered for non-state
|
||||
# events.
|
||||
if not event.is_state():
|
||||
retention_policy = retention_policies[event.room_id]
|
||||
max_lifetime = retention_policy.max_lifetime
|
||||
|
||||
if max_lifetime is not None:
|
||||
oldest_allowed_ts = storage.main.clock.time_msec() - max_lifetime
|
||||
|
||||
if event.origin_server_ts < oldest_allowed_ts:
|
||||
return None
|
||||
|
||||
if event.event_id in always_include_ids:
|
||||
return event
|
||||
|
||||
# we need to handle outliers separately, since we don't have the room state.
|
||||
if event.internal_metadata.outlier:
|
||||
# Normally these can't be seen by clients, but we make an exception for
|
||||
# for out-of-band membership events (eg, incoming invites, or rejections of
|
||||
# said invite) for the user themselves.
|
||||
if event.type == EventTypes.Member and event.state_key == user_id:
|
||||
logger.debug("Returning out-of-band-membership event %s", event)
|
||||
return event
|
||||
|
||||
return None
|
||||
|
||||
state = event_id_to_state[event.event_id]
|
||||
|
||||
# get the room_visibility at the time of the event.
|
||||
visibility = get_effective_room_visibility_from_state(state)
|
||||
|
||||
# Always allow history visibility events on boundaries. This is done
|
||||
# by setting the effective visibility to the least restrictive
|
||||
# of the old vs new.
|
||||
if event.type == EventTypes.RoomHistoryVisibility:
|
||||
prev_content = event.unsigned.get("prev_content", {})
|
||||
prev_visibility = prev_content.get("history_visibility", None)
|
||||
|
||||
if prev_visibility not in VISIBILITY_PRIORITY:
|
||||
prev_visibility = HistoryVisibility.SHARED
|
||||
|
||||
new_priority = VISIBILITY_PRIORITY.index(visibility)
|
||||
old_priority = VISIBILITY_PRIORITY.index(prev_visibility)
|
||||
if old_priority < new_priority:
|
||||
visibility = prev_visibility
|
||||
|
||||
# likewise, if the event is the user's own membership event, use
|
||||
# the 'most joined' membership
|
||||
membership = None
|
||||
if event.type == EventTypes.Member and event.state_key == user_id:
|
||||
membership = event.content.get("membership", None)
|
||||
if membership not in MEMBERSHIP_PRIORITY:
|
||||
membership = "leave"
|
||||
|
||||
prev_content = event.unsigned.get("prev_content", {})
|
||||
prev_membership = prev_content.get("membership", None)
|
||||
if prev_membership not in MEMBERSHIP_PRIORITY:
|
||||
prev_membership = "leave"
|
||||
|
||||
# Always allow the user to see their own leave events, otherwise
|
||||
# they won't see the room disappear if they reject the invite
|
||||
#
|
||||
# (Note this doesn't work for out-of-band invite rejections, which don't
|
||||
# have prev_state populated. They are handled above in the outlier code.)
|
||||
if membership == "leave" and (
|
||||
prev_membership == "join" or prev_membership == "invite"
|
||||
):
|
||||
return event
|
||||
|
||||
new_priority = MEMBERSHIP_PRIORITY.index(membership)
|
||||
old_priority = MEMBERSHIP_PRIORITY.index(prev_membership)
|
||||
if old_priority < new_priority:
|
||||
membership = prev_membership
|
||||
|
||||
# otherwise, get the user's membership at the time of the event.
|
||||
if membership is None:
|
||||
membership_event = state.get((EventTypes.Member, user_id), None)
|
||||
if membership_event:
|
||||
membership = membership_event.membership
|
||||
|
||||
# if the user was a member of the room at the time of the event,
|
||||
# they can see it.
|
||||
if membership == Membership.JOIN:
|
||||
return event
|
||||
|
||||
# otherwise, it depends on the room visibility.
|
||||
|
||||
if visibility == HistoryVisibility.JOINED:
|
||||
# we weren't a member at the time of the event, so we can't
|
||||
# see this event.
|
||||
return None
|
||||
|
||||
elif visibility == HistoryVisibility.INVITED:
|
||||
# user can also see the event if they were *invited* at the time
|
||||
# of the event.
|
||||
return event if membership == Membership.INVITE else None
|
||||
|
||||
elif visibility == HistoryVisibility.SHARED and is_peeking:
|
||||
# if the visibility is shared, users cannot see the event unless
|
||||
# they have *subsequently* joined the room (or were members at the
|
||||
# time, of course)
|
||||
#
|
||||
# XXX: if the user has subsequently joined and then left again,
|
||||
# ideally we would share history up to the point they left. But
|
||||
# we don't know when they left. We just treat it as though they
|
||||
# never joined, and restrict access.
|
||||
return None
|
||||
|
||||
# the visibility is either shared or world_readable, and the user was
|
||||
# not a member at the time. We allow it, provided the original sender
|
||||
# has not requested their data to be erased, in which case, we return
|
||||
# a redacted version.
|
||||
if erased_senders[event.sender]:
|
||||
return prune_event(event)
|
||||
|
||||
return event
|
||||
|
||||
# Check each event: gives an iterable of None or (a potentially modified)
|
||||
# EventBase.
|
||||
@@ -128,389 +258,9 @@ async def filter_events_for_client(
|
||||
return [ev for ev in filtered_events if ev]
|
||||
|
||||
|
||||
async def filter_event_for_clients_with_state(
|
||||
store: DataStore,
|
||||
user_ids: Collection[str],
|
||||
event: EventBase,
|
||||
context: EventContext,
|
||||
is_peeking: bool = False,
|
||||
filter_send_to_client: bool = True,
|
||||
) -> Collection[str]:
|
||||
"""
|
||||
Checks to see if an event is visible to the users in the list at the time of
|
||||
the event.
|
||||
|
||||
Note: This does *not* check if the sender of the event was erased.
|
||||
|
||||
Args:
|
||||
store: databases
|
||||
user_ids: user_ids to be checked
|
||||
event: the event to be checked
|
||||
context: EventContext for the event to be checked
|
||||
is_peeking: Whether the users are peeking into the room, ie not
|
||||
currently joined
|
||||
filter_send_to_client: Whether we're checking an event that's going to be
|
||||
sent to a client. This might not always be the case since this function can
|
||||
also be called to check whether a user can see the state at a given point.
|
||||
|
||||
Returns:
|
||||
Collection of user IDs for whom the event is visible
|
||||
"""
|
||||
# None of the users should see the event if it is soft_failed
|
||||
if event.internal_metadata.is_soft_failed():
|
||||
return []
|
||||
|
||||
# Make a set for all user IDs that haven't been filtered out by a check.
|
||||
allowed_user_ids = set(user_ids)
|
||||
|
||||
# Only run some checks if these events aren't about to be sent to clients. This is
|
||||
# because, if this is not the case, we're probably only checking if the users can
|
||||
# see events in the room at that point in the DAG, and that shouldn't be decided
|
||||
# on those checks.
|
||||
if filter_send_to_client:
|
||||
ignored_by = await store.ignored_by(event.sender)
|
||||
retention_policy = await store.get_retention_policy_for_room(event.room_id)
|
||||
|
||||
for user_id in user_ids:
|
||||
if (
|
||||
_check_filter_send_to_client(
|
||||
event,
|
||||
store.clock,
|
||||
retention_policy,
|
||||
sender_ignored=user_id in ignored_by,
|
||||
)
|
||||
== _CheckFilter.DENIED
|
||||
):
|
||||
allowed_user_ids.discard(user_id)
|
||||
|
||||
if event.internal_metadata.outlier:
|
||||
# Normally these can't be seen by clients, but we make an exception for
|
||||
# for out-of-band membership events (eg, incoming invites, or rejections of
|
||||
# said invite) for the user themselves.
|
||||
if event.type == EventTypes.Member and event.state_key in allowed_user_ids:
|
||||
logger.debug("Returning out-of-band-membership event %s", event)
|
||||
return {event.state_key}
|
||||
|
||||
return set()
|
||||
|
||||
# First we get just the history visibility in case its shared/world-readable
|
||||
# room.
|
||||
visibility_state_map = await _get_state_map(
|
||||
store, event, context, StateFilter.from_types([_HISTORY_VIS_KEY])
|
||||
)
|
||||
|
||||
visibility = get_effective_room_visibility_from_state(visibility_state_map)
|
||||
if (
|
||||
_check_history_visibility(event, visibility, is_peeking=is_peeking)
|
||||
== _CheckVisibility.ALLOWED
|
||||
):
|
||||
return allowed_user_ids
|
||||
|
||||
# The history visibility isn't lax, so we now need to fetch the membership
|
||||
# events of all the users.
|
||||
|
||||
filter_list = []
|
||||
for user_id in allowed_user_ids:
|
||||
filter_list.append((EventTypes.Member, user_id))
|
||||
filter_list.append((EventTypes.RoomHistoryVisibility, ""))
|
||||
|
||||
state_filter = StateFilter.from_types(filter_list)
|
||||
state_map = await _get_state_map(store, event, context, state_filter)
|
||||
|
||||
# Now we check whether the membership allows each user to see the event.
|
||||
return {
|
||||
user_id
|
||||
for user_id in allowed_user_ids
|
||||
if _check_membership(user_id, event, visibility, state_map, is_peeking).allowed
|
||||
}
|
||||
|
||||
|
||||
async def _get_state_map(
|
||||
store: DataStore, event: EventBase, context: EventContext, state_filter: StateFilter
|
||||
) -> StateMap[EventBase]:
|
||||
"""Helper function for getting a `StateMap[EventBase]` from an `EventContext`"""
|
||||
state_map = await context.get_prev_state_ids(state_filter)
|
||||
|
||||
# Use events rather than event ids as content from the events are needed in
|
||||
# _check_visibility
|
||||
event_map = await store.get_events(state_map.values(), get_prev_content=False)
|
||||
|
||||
updated_state_map = {}
|
||||
for state_key, event_id in state_map.items():
|
||||
state_event = event_map.get(event_id)
|
||||
if state_event:
|
||||
updated_state_map[state_key] = state_event
|
||||
|
||||
if event.is_state():
|
||||
current_state_key = (event.type, event.state_key)
|
||||
# Add current event to updated_state_map, we need to do this here as it
|
||||
# may not have been persisted to the db yet
|
||||
updated_state_map[current_state_key] = event
|
||||
|
||||
return updated_state_map
|
||||
|
||||
|
||||
def _check_client_allowed_to_see_event(
|
||||
user_id: str,
|
||||
event: EventBase,
|
||||
clock: Clock,
|
||||
filter_send_to_client: bool,
|
||||
is_peeking: bool,
|
||||
always_include_ids: FrozenSet[str],
|
||||
sender_ignored: bool,
|
||||
retention_policy: RetentionPolicy,
|
||||
state: Optional[StateMap[EventBase]],
|
||||
sender_erased: bool,
|
||||
) -> Optional[EventBase]:
|
||||
"""Check with the given user is allowed to see the given event
|
||||
|
||||
See `filter_events_for_client` for details about args
|
||||
|
||||
Args:
|
||||
user_id
|
||||
event
|
||||
clock
|
||||
filter_send_to_client
|
||||
is_peeking
|
||||
always_include_ids
|
||||
sender_ignored: Whether the user is ignoring the event sender
|
||||
retention_policy: The retention policy of the room
|
||||
state: The state at the event, unless its an outlier
|
||||
sender_erased: Whether the event sender has been marked as "erased"
|
||||
|
||||
Returns:
|
||||
None if the user cannot see this event at all
|
||||
|
||||
a redacted copy of the event if they can only see a redacted
|
||||
version
|
||||
|
||||
the original event if they can see it as normal.
|
||||
"""
|
||||
# Only run some checks if these events aren't about to be sent to clients. This is
|
||||
# because, if this is not the case, we're probably only checking if the users can
|
||||
# see events in the room at that point in the DAG, and that shouldn't be decided
|
||||
# on those checks.
|
||||
if filter_send_to_client:
|
||||
if (
|
||||
_check_filter_send_to_client(event, clock, retention_policy, sender_ignored)
|
||||
== _CheckFilter.DENIED
|
||||
):
|
||||
return None
|
||||
|
||||
if event.event_id in always_include_ids:
|
||||
return event
|
||||
|
||||
# we need to handle outliers separately, since we don't have the room state.
|
||||
if event.internal_metadata.outlier:
|
||||
# Normally these can't be seen by clients, but we make an exception for
|
||||
# for out-of-band membership events (eg, incoming invites, or rejections of
|
||||
# said invite) for the user themselves.
|
||||
if event.type == EventTypes.Member and event.state_key == user_id:
|
||||
logger.debug("Returning out-of-band-membership event %s", event)
|
||||
return event
|
||||
|
||||
return None
|
||||
|
||||
if state is None:
|
||||
raise Exception("Missing state for non-outlier event")
|
||||
|
||||
# get the room_visibility at the time of the event.
|
||||
visibility = get_effective_room_visibility_from_state(state)
|
||||
|
||||
# Check if the room has lax history visibility, allowing us to skip
|
||||
# membership checks.
|
||||
#
|
||||
# We can only do this check if the sender has *not* been erased, as if they
|
||||
# have we need to check the user's membership.
|
||||
if (
|
||||
not sender_erased
|
||||
and _check_history_visibility(event, visibility, is_peeking)
|
||||
== _CheckVisibility.ALLOWED
|
||||
):
|
||||
return event
|
||||
|
||||
membership_result = _check_membership(user_id, event, visibility, state, is_peeking)
|
||||
if not membership_result.allowed:
|
||||
return None
|
||||
|
||||
# If the sender has been erased and the user was not joined at the time, we
|
||||
# must only return the redacted form.
|
||||
if sender_erased and not membership_result.joined:
|
||||
event = prune_event(event)
|
||||
|
||||
return event
|
||||
|
||||
|
||||
@attr.s(frozen=True, slots=True, auto_attribs=True)
|
||||
class _CheckMembershipReturn:
|
||||
"Return value of _check_membership"
|
||||
allowed: bool
|
||||
joined: bool
|
||||
|
||||
|
||||
def _check_membership(
|
||||
user_id: str,
|
||||
event: EventBase,
|
||||
visibility: str,
|
||||
state: StateMap[EventBase],
|
||||
is_peeking: bool,
|
||||
) -> _CheckMembershipReturn:
|
||||
"""Check whether the user can see the event due to their membership
|
||||
|
||||
Returns:
|
||||
True if they can, False if they can't, plus the membership of the user
|
||||
at the event.
|
||||
"""
|
||||
# If the event is the user's own membership event, use the 'most joined'
|
||||
# membership
|
||||
membership = None
|
||||
if event.type == EventTypes.Member and event.state_key == user_id:
|
||||
membership = event.content.get("membership", None)
|
||||
if membership not in MEMBERSHIP_PRIORITY:
|
||||
membership = "leave"
|
||||
|
||||
prev_content = event.unsigned.get("prev_content", {})
|
||||
prev_membership = prev_content.get("membership", None)
|
||||
if prev_membership not in MEMBERSHIP_PRIORITY:
|
||||
prev_membership = "leave"
|
||||
|
||||
# Always allow the user to see their own leave events, otherwise
|
||||
# they won't see the room disappear if they reject the invite
|
||||
#
|
||||
# (Note this doesn't work for out-of-band invite rejections, which don't
|
||||
# have prev_state populated. They are handled above in the outlier code.)
|
||||
if membership == "leave" and (
|
||||
prev_membership == "join" or prev_membership == "invite"
|
||||
):
|
||||
return _CheckMembershipReturn(True, membership == Membership.JOIN)
|
||||
|
||||
new_priority = MEMBERSHIP_PRIORITY.index(membership)
|
||||
old_priority = MEMBERSHIP_PRIORITY.index(prev_membership)
|
||||
if old_priority < new_priority:
|
||||
membership = prev_membership
|
||||
|
||||
# otherwise, get the user's membership at the time of the event.
|
||||
if membership is None:
|
||||
membership_event = state.get((EventTypes.Member, user_id), None)
|
||||
if membership_event:
|
||||
membership = membership_event.membership
|
||||
|
||||
# if the user was a member of the room at the time of the event,
|
||||
# they can see it.
|
||||
if membership == Membership.JOIN:
|
||||
return _CheckMembershipReturn(True, True)
|
||||
|
||||
# otherwise, it depends on the room visibility.
|
||||
|
||||
if visibility == HistoryVisibility.JOINED:
|
||||
# we weren't a member at the time of the event, so we can't
|
||||
# see this event.
|
||||
return _CheckMembershipReturn(False, False)
|
||||
|
||||
elif visibility == HistoryVisibility.INVITED:
|
||||
# user can also see the event if they were *invited* at the time
|
||||
# of the event.
|
||||
return _CheckMembershipReturn(membership == Membership.INVITE, False)
|
||||
|
||||
elif visibility == HistoryVisibility.SHARED and is_peeking:
|
||||
# if the visibility is shared, users cannot see the event unless
|
||||
# they have *subsequently* joined the room (or were members at the
|
||||
# time, of course)
|
||||
#
|
||||
# XXX: if the user has subsequently joined and then left again,
|
||||
# ideally we would share history up to the point they left. But
|
||||
# we don't know when they left. We just treat it as though they
|
||||
# never joined, and restrict access.
|
||||
return _CheckMembershipReturn(False, False)
|
||||
|
||||
# The visibility is either shared or world_readable, and the user was
|
||||
# not a member at the time. We allow it.
|
||||
return _CheckMembershipReturn(True, False)
|
||||
|
||||
|
||||
class _CheckFilter(Enum):
|
||||
MAYBE_ALLOWED = auto()
|
||||
DENIED = auto()
|
||||
|
||||
|
||||
def _check_filter_send_to_client(
|
||||
event: EventBase,
|
||||
clock: Clock,
|
||||
retention_policy: RetentionPolicy,
|
||||
sender_ignored: bool,
|
||||
) -> _CheckFilter:
|
||||
"""Apply checks for sending events to client
|
||||
|
||||
Returns:
|
||||
True if might be allowed to be sent to clients, False if definitely not.
|
||||
"""
|
||||
|
||||
if event.type == EventTypes.Dummy:
|
||||
return _CheckFilter.DENIED
|
||||
|
||||
if not event.is_state() and sender_ignored:
|
||||
return _CheckFilter.DENIED
|
||||
|
||||
# Until MSC2261 has landed we can't redact malicious alias events, so for
|
||||
# now we temporarily filter out m.room.aliases entirely to mitigate
|
||||
# abuse, while we spec a better solution to advertising aliases
|
||||
# on rooms.
|
||||
if event.type == EventTypes.Aliases:
|
||||
return _CheckFilter.DENIED
|
||||
|
||||
# Don't try to apply the room's retention policy if the event is a state
|
||||
# event, as MSC1763 states that retention is only considered for non-state
|
||||
# events.
|
||||
if not event.is_state():
|
||||
max_lifetime = retention_policy.max_lifetime
|
||||
|
||||
if max_lifetime is not None:
|
||||
oldest_allowed_ts = clock.time_msec() - max_lifetime
|
||||
|
||||
if event.origin_server_ts < oldest_allowed_ts:
|
||||
return _CheckFilter.DENIED
|
||||
|
||||
return _CheckFilter.MAYBE_ALLOWED
|
||||
|
||||
|
||||
class _CheckVisibility(Enum):
|
||||
ALLOWED = auto()
|
||||
MAYBE_DENIED = auto()
|
||||
|
||||
|
||||
def _check_history_visibility(
|
||||
event: EventBase, visibility: str, is_peeking: bool
|
||||
) -> _CheckVisibility:
|
||||
"""Check if event is allowed to be seen due to lax history visibility.
|
||||
|
||||
Returns:
|
||||
True if user can definitely see the event, False if maybe not.
|
||||
"""
|
||||
# Always allow history visibility events on boundaries. This is done
|
||||
# by setting the effective visibility to the least restrictive
|
||||
# of the old vs new.
|
||||
if event.type == EventTypes.RoomHistoryVisibility:
|
||||
prev_content = event.unsigned.get("prev_content", {})
|
||||
prev_visibility = prev_content.get("history_visibility", None)
|
||||
|
||||
if prev_visibility not in VISIBILITY_PRIORITY:
|
||||
prev_visibility = HistoryVisibility.SHARED
|
||||
|
||||
new_priority = VISIBILITY_PRIORITY.index(visibility)
|
||||
old_priority = VISIBILITY_PRIORITY.index(prev_visibility)
|
||||
if old_priority < new_priority:
|
||||
visibility = prev_visibility
|
||||
|
||||
if visibility == HistoryVisibility.SHARED and not is_peeking:
|
||||
return _CheckVisibility.ALLOWED
|
||||
elif visibility == HistoryVisibility.WORLD_READABLE:
|
||||
return _CheckVisibility.ALLOWED
|
||||
|
||||
return _CheckVisibility.MAYBE_DENIED
|
||||
|
||||
|
||||
def get_effective_room_visibility_from_state(state: StateMap[EventBase]) -> str:
|
||||
"""Get the actual history vis, from a state map including the history_visibility event
|
||||
|
||||
Handles missing and invalid history visibility events.
|
||||
"""
|
||||
visibility_event = state.get(_HISTORY_VIS_KEY, None)
|
||||
|
||||
@@ -16,23 +16,13 @@ from typing import Dict, Optional, Set, Tuple, Union
|
||||
|
||||
import frozendict
|
||||
|
||||
from twisted.test.proto_helpers import MemoryReactor
|
||||
|
||||
import synapse.rest.admin
|
||||
from synapse.api.constants import EventTypes, Membership
|
||||
from synapse.api.room_versions import RoomVersions
|
||||
from synapse.appservice import ApplicationService
|
||||
from synapse.events import FrozenEvent
|
||||
from synapse.push import push_rule_evaluator
|
||||
from synapse.push.push_rule_evaluator import PushRuleEvaluatorForEvent
|
||||
from synapse.rest.client import login, register, room
|
||||
from synapse.server import HomeServer
|
||||
from synapse.storage.databases.main.appservice import _make_exclusive_regex
|
||||
from synapse.types import JsonDict
|
||||
from synapse.util import Clock
|
||||
|
||||
from tests import unittest
|
||||
from tests.test_utils.event_injection import create_event, inject_member_event
|
||||
|
||||
|
||||
class PushRuleEvaluatorTestCase(unittest.TestCase):
|
||||
@@ -364,78 +354,3 @@ class PushRuleEvaluatorTestCase(unittest.TestCase):
|
||||
"event_type": "*.reaction",
|
||||
}
|
||||
self.assertTrue(evaluator.matches(condition, "@user:test", "foo"))
|
||||
|
||||
|
||||
class TestBulkPushRuleEvaluator(unittest.HomeserverTestCase):
|
||||
"""Tests for the bulk push rule evaluator"""
|
||||
|
||||
servlets = [
|
||||
synapse.rest.admin.register_servlets_for_client_rest_resource,
|
||||
login.register_servlets,
|
||||
register.register_servlets,
|
||||
room.register_servlets,
|
||||
]
|
||||
|
||||
def prepare(self, reactor: MemoryReactor, clock: Clock, homeserver: HomeServer):
|
||||
# Define an application service so that we can register appservice users
|
||||
self._service_token = "some_token"
|
||||
self._service = ApplicationService(
|
||||
self._service_token,
|
||||
"as1",
|
||||
"@as.sender:test",
|
||||
namespaces={
|
||||
"users": [
|
||||
{"regex": "@_as_.*:test", "exclusive": True},
|
||||
{"regex": "@as.sender:test", "exclusive": True},
|
||||
]
|
||||
},
|
||||
msc3202_transaction_extensions=True,
|
||||
)
|
||||
self.hs.get_datastores().main.services_cache = [self._service]
|
||||
self.hs.get_datastores().main.exclusive_user_regex = _make_exclusive_regex(
|
||||
[self._service]
|
||||
)
|
||||
|
||||
self._as_user, _ = self.register_appservice_user(
|
||||
"_as_user", self._service_token
|
||||
)
|
||||
|
||||
self.evaluator = self.hs.get_bulk_push_rule_evaluator()
|
||||
|
||||
def test_ignore_appservice_users(self) -> None:
|
||||
"Test that we don't generate push for appservice users"
|
||||
|
||||
user_id = self.register_user("user", "pass")
|
||||
token = self.login("user", "pass")
|
||||
|
||||
room_id = self.helper.create_room_as(user_id, tok=token)
|
||||
self.get_success(
|
||||
inject_member_event(self.hs, room_id, self._as_user, Membership.JOIN)
|
||||
)
|
||||
|
||||
event, context = self.get_success(
|
||||
create_event(
|
||||
self.hs,
|
||||
type=EventTypes.Message,
|
||||
room_id=room_id,
|
||||
sender=user_id,
|
||||
content={"body": "test", "msgtype": "m.text"},
|
||||
)
|
||||
)
|
||||
|
||||
# Assert the returned push rules do not contain the app service user
|
||||
rules = self.get_success(self.evaluator._get_rules_for_event(event))
|
||||
self.assertTrue(self._as_user not in rules)
|
||||
|
||||
# Assert that no push actions have been added to the staging table (the
|
||||
# sender should not be pushed for the event)
|
||||
users_with_push_actions = self.get_success(
|
||||
self.hs.get_datastores().main.db_pool.simple_select_onecol(
|
||||
table="event_push_actions_staging",
|
||||
keyvalues={"event_id": event.event_id},
|
||||
retcol="user_id",
|
||||
desc="test_ignore_appservice_users",
|
||||
)
|
||||
)
|
||||
|
||||
self.assertEqual(len(users_with_push_actions), 0)
|
||||
|
||||
@@ -22,7 +22,7 @@ from typing import Any, Dict, Iterable, List, Optional, Tuple, Union
|
||||
from unittest.mock import Mock, call
|
||||
from urllib import parse as urlparse
|
||||
|
||||
from parameterized import param, parameterized
|
||||
# `Literal` appears with Python 3.8.
|
||||
from typing_extensions import Literal
|
||||
|
||||
from twisted.test.proto_helpers import MemoryReactor
|
||||
@@ -708,21 +708,6 @@ class RoomsCreateTestCase(RoomBase):
|
||||
|
||||
self.assertEqual(200, channel.code, channel.result)
|
||||
self.assertTrue("room_id" in channel.json_body)
|
||||
assert channel.resource_usage is not None
|
||||
self.assertEqual(37, channel.resource_usage.db_txn_count)
|
||||
|
||||
def test_post_room_initial_state(self) -> None:
|
||||
# POST with initial_state config key, expect new room id
|
||||
channel = self.make_request(
|
||||
"POST",
|
||||
"/createRoom",
|
||||
b'{"initial_state":[{"type": "m.bridge", "content": {}}]}',
|
||||
)
|
||||
|
||||
self.assertEqual(200, channel.code, channel.result)
|
||||
self.assertTrue("room_id" in channel.json_body)
|
||||
assert channel.resource_usage is not None
|
||||
self.assertEqual(41, channel.resource_usage.db_txn_count)
|
||||
|
||||
def test_post_room_visibility_key(self) -> None:
|
||||
# POST with visibility config key, expect new room id
|
||||
@@ -830,14 +815,14 @@ class RoomsCreateTestCase(RoomBase):
|
||||
In this test, we use the more recent API in which callbacks return a `Union[Codes, Literal["NOT_SPAM"]]`.
|
||||
"""
|
||||
|
||||
async def user_may_join_room_codes(
|
||||
async def user_may_join_room(
|
||||
mxid: str,
|
||||
room_id: str,
|
||||
is_invite: bool,
|
||||
) -> Codes:
|
||||
return Codes.CONSENT_NOT_GIVEN
|
||||
|
||||
join_mock = Mock(side_effect=user_may_join_room_codes)
|
||||
join_mock = Mock(side_effect=user_may_join_room)
|
||||
self.hs.get_spam_checker()._user_may_join_room_callbacks.append(join_mock)
|
||||
|
||||
channel = self.make_request(
|
||||
@@ -849,25 +834,6 @@ class RoomsCreateTestCase(RoomBase):
|
||||
|
||||
self.assertEqual(join_mock.call_count, 0)
|
||||
|
||||
# Now change the return value of the callback to deny any join. Since we're
|
||||
# creating the room, despite the return value, we should be able to join.
|
||||
async def user_may_join_room_tuple(
|
||||
mxid: str,
|
||||
room_id: str,
|
||||
is_invite: bool,
|
||||
) -> Tuple[Codes, dict]:
|
||||
return Codes.INCOMPATIBLE_ROOM_VERSION, {}
|
||||
|
||||
join_mock.side_effect = user_may_join_room_tuple
|
||||
|
||||
channel = self.make_request(
|
||||
"POST",
|
||||
"/createRoom",
|
||||
{},
|
||||
)
|
||||
self.assertEqual(channel.code, 200, channel.json_body)
|
||||
self.assertEqual(join_mock.call_count, 0)
|
||||
|
||||
|
||||
class RoomTopicTestCase(RoomBase):
|
||||
"""Tests /rooms/$room_id/topic REST events."""
|
||||
@@ -1147,15 +1113,13 @@ class RoomJoinTestCase(RoomBase):
|
||||
"""
|
||||
|
||||
# Register a dummy callback. Make it allow all room joins for now.
|
||||
return_value: Union[
|
||||
Literal["NOT_SPAM"], Tuple[Codes, dict], Codes
|
||||
] = synapse.module_api.NOT_SPAM
|
||||
return_value: Union[Literal["NOT_SPAM"], Codes] = synapse.module_api.NOT_SPAM
|
||||
|
||||
async def user_may_join_room(
|
||||
userid: str,
|
||||
room_id: str,
|
||||
is_invited: bool,
|
||||
) -> Union[Literal["NOT_SPAM"], Tuple[Codes, dict], Codes]:
|
||||
) -> Union[Literal["NOT_SPAM"], Codes]:
|
||||
return return_value
|
||||
|
||||
# `spec` argument is needed for this function mock to have `__qualname__`, which
|
||||
@@ -1199,28 +1163,8 @@ class RoomJoinTestCase(RoomBase):
|
||||
)
|
||||
|
||||
# Now make the callback deny all room joins, and check that a join actually fails.
|
||||
# We pick an arbitrary Codes rather than the default `Codes.FORBIDDEN`.
|
||||
return_value = Codes.CONSENT_NOT_GIVEN
|
||||
self.helper.invite(self.room3, self.user1, self.user2, tok=self.tok1)
|
||||
self.helper.join(
|
||||
self.room3,
|
||||
self.user2,
|
||||
expect_code=403,
|
||||
expect_errcode=return_value,
|
||||
tok=self.tok2,
|
||||
)
|
||||
|
||||
# Now make the callback deny all room joins, and check that a join actually fails.
|
||||
# As above, with the experimental extension that lets us return dictionaries.
|
||||
return_value = (Codes.BAD_ALIAS, {"another_field": "12345"})
|
||||
self.helper.join(
|
||||
self.room3,
|
||||
self.user2,
|
||||
expect_code=403,
|
||||
expect_errcode=return_value[0],
|
||||
tok=self.tok2,
|
||||
expect_additional_fields=return_value[1],
|
||||
)
|
||||
self.helper.join(self.room3, self.user2, expect_code=403, tok=self.tok2)
|
||||
|
||||
|
||||
class RoomJoinRatelimitTestCase(RoomBase):
|
||||
@@ -1370,97 +1314,6 @@ class RoomMessagesTestCase(RoomBase):
|
||||
channel = self.make_request("PUT", path, content)
|
||||
self.assertEqual(200, channel.code, msg=channel.result["body"])
|
||||
|
||||
@parameterized.expand(
|
||||
[
|
||||
# Allow
|
||||
param(
|
||||
name="NOT_SPAM", value="NOT_SPAM", expected_code=200, expected_fields={}
|
||||
),
|
||||
param(name="False", value=False, expected_code=200, expected_fields={}),
|
||||
# Block
|
||||
param(
|
||||
name="scalene string",
|
||||
value="ANY OTHER STRING",
|
||||
expected_code=403,
|
||||
expected_fields={"errcode": "M_FORBIDDEN"},
|
||||
),
|
||||
param(
|
||||
name="True",
|
||||
value=True,
|
||||
expected_code=403,
|
||||
expected_fields={"errcode": "M_FORBIDDEN"},
|
||||
),
|
||||
param(
|
||||
name="Code",
|
||||
value=Codes.LIMIT_EXCEEDED,
|
||||
expected_code=403,
|
||||
expected_fields={"errcode": "M_LIMIT_EXCEEDED"},
|
||||
),
|
||||
param(
|
||||
name="Tuple",
|
||||
value=(Codes.SERVER_NOT_TRUSTED, {"additional_field": "12345"}),
|
||||
expected_code=403,
|
||||
expected_fields={
|
||||
"errcode": "M_SERVER_NOT_TRUSTED",
|
||||
"additional_field": "12345",
|
||||
},
|
||||
),
|
||||
]
|
||||
)
|
||||
def test_spam_checker_check_event_for_spam(
|
||||
self,
|
||||
name: str,
|
||||
value: Union[str, bool, Codes, Tuple[Codes, JsonDict]],
|
||||
expected_code: int,
|
||||
expected_fields: dict,
|
||||
) -> None:
|
||||
class SpamCheck:
|
||||
mock_return_value: Union[
|
||||
str, bool, Codes, Tuple[Codes, JsonDict], bool
|
||||
] = "NOT_SPAM"
|
||||
mock_content: Optional[JsonDict] = None
|
||||
|
||||
async def check_event_for_spam(
|
||||
self,
|
||||
event: synapse.events.EventBase,
|
||||
) -> Union[str, Codes, Tuple[Codes, JsonDict], bool]:
|
||||
self.mock_content = event.content
|
||||
return self.mock_return_value
|
||||
|
||||
spam_checker = SpamCheck()
|
||||
|
||||
self.hs.get_spam_checker()._check_event_for_spam_callbacks.append(
|
||||
spam_checker.check_event_for_spam
|
||||
)
|
||||
|
||||
# Inject `value` as mock_return_value
|
||||
spam_checker.mock_return_value = value
|
||||
path = "/rooms/%s/send/m.room.message/check_event_for_spam_%s" % (
|
||||
urlparse.quote(self.room_id),
|
||||
urlparse.quote(name),
|
||||
)
|
||||
body = "test-%s" % name
|
||||
content = '{"body":"%s","msgtype":"m.text"}' % body
|
||||
channel = self.make_request("PUT", path, content)
|
||||
|
||||
# Check that the callback has witnessed the correct event.
|
||||
self.assertIsNotNone(spam_checker.mock_content)
|
||||
if (
|
||||
spam_checker.mock_content is not None
|
||||
): # Checked just above, but mypy doesn't know about that.
|
||||
self.assertEqual(
|
||||
spam_checker.mock_content["body"], body, spam_checker.mock_content
|
||||
)
|
||||
|
||||
# Check that we have the correct result.
|
||||
self.assertEqual(expected_code, channel.code, msg=channel.result["body"])
|
||||
for expected_key, expected_value in expected_fields.items():
|
||||
self.assertEqual(
|
||||
channel.json_body.get(expected_key, None),
|
||||
expected_value,
|
||||
"Field %s absent or invalid " % expected_key,
|
||||
)
|
||||
|
||||
|
||||
class RoomPowerLevelOverridesTestCase(RoomBase):
|
||||
"""Tests that the power levels can be overridden with server config."""
|
||||
@@ -3382,8 +3235,7 @@ class ThreepidInviteTestCase(unittest.HomeserverTestCase):
|
||||
make_invite_mock.assert_called_once()
|
||||
|
||||
# Now change the return value of the callback to deny any invite and test that
|
||||
# we can't send the invite. We pick an arbitrary error code to be able to check
|
||||
# that the same code has been returned
|
||||
# we can't send the invite.
|
||||
mock.return_value = make_awaitable(Codes.CONSENT_NOT_GIVEN)
|
||||
channel = self.make_request(
|
||||
method="POST",
|
||||
@@ -3397,27 +3249,6 @@ class ThreepidInviteTestCase(unittest.HomeserverTestCase):
|
||||
access_token=self.tok,
|
||||
)
|
||||
self.assertEqual(channel.code, 403)
|
||||
self.assertEqual(channel.json_body["errcode"], Codes.CONSENT_NOT_GIVEN)
|
||||
|
||||
# Also check that it stopped before calling _make_and_store_3pid_invite.
|
||||
make_invite_mock.assert_called_once()
|
||||
|
||||
# Run variant with `Tuple[Codes, dict]`.
|
||||
mock.return_value = make_awaitable((Codes.EXPIRED_ACCOUNT, {"field": "value"}))
|
||||
channel = self.make_request(
|
||||
method="POST",
|
||||
path="/rooms/" + self.room_id + "/invite",
|
||||
content={
|
||||
"id_server": "example.com",
|
||||
"id_access_token": "sometoken",
|
||||
"medium": "email",
|
||||
"address": email_to_invite,
|
||||
},
|
||||
access_token=self.tok,
|
||||
)
|
||||
self.assertEqual(channel.code, 403)
|
||||
self.assertEqual(channel.json_body["errcode"], Codes.EXPIRED_ACCOUNT)
|
||||
self.assertEqual(channel.json_body["field"], "value")
|
||||
|
||||
# Also check that it stopped before calling _make_and_store_3pid_invite.
|
||||
make_invite_mock.assert_called_once()
|
||||
|
||||
@@ -41,7 +41,6 @@ from twisted.web.resource import Resource
|
||||
from twisted.web.server import Site
|
||||
|
||||
from synapse.api.constants import Membership
|
||||
from synapse.api.errors import Codes
|
||||
from synapse.server import HomeServer
|
||||
from synapse.types import JsonDict
|
||||
|
||||
@@ -172,8 +171,6 @@ class RestHelper:
|
||||
expect_code: int = HTTPStatus.OK,
|
||||
tok: Optional[str] = None,
|
||||
appservice_user_id: Optional[str] = None,
|
||||
expect_errcode: Optional[Codes] = None,
|
||||
expect_additional_fields: Optional[dict] = None,
|
||||
) -> None:
|
||||
self.change_membership(
|
||||
room=room,
|
||||
@@ -183,8 +180,6 @@ class RestHelper:
|
||||
appservice_user_id=appservice_user_id,
|
||||
membership=Membership.JOIN,
|
||||
expect_code=expect_code,
|
||||
expect_errcode=expect_errcode,
|
||||
expect_additional_fields=expect_additional_fields,
|
||||
)
|
||||
|
||||
def knock(
|
||||
@@ -268,7 +263,6 @@ class RestHelper:
|
||||
appservice_user_id: Optional[str] = None,
|
||||
expect_code: int = HTTPStatus.OK,
|
||||
expect_errcode: Optional[str] = None,
|
||||
expect_additional_fields: Optional[dict] = None,
|
||||
) -> None:
|
||||
"""
|
||||
Send a membership state event into a room.
|
||||
@@ -329,21 +323,6 @@ class RestHelper:
|
||||
channel.result["body"],
|
||||
)
|
||||
|
||||
if expect_additional_fields is not None:
|
||||
for expect_key, expect_value in expect_additional_fields.items():
|
||||
assert expect_key in channel.json_body, "Expected field %s, got %s" % (
|
||||
expect_key,
|
||||
channel.json_body,
|
||||
)
|
||||
assert (
|
||||
channel.json_body[expect_key] == expect_value
|
||||
), "Expected: %s at %s, got: %s, resp: %s" % (
|
||||
expect_value,
|
||||
expect_key,
|
||||
channel.json_body[expect_key],
|
||||
channel.json_body,
|
||||
)
|
||||
|
||||
self.auth_user_id = temp_id
|
||||
|
||||
def send(
|
||||
|
||||
@@ -23,13 +23,11 @@ from urllib import parse
|
||||
import attr
|
||||
from parameterized import parameterized, parameterized_class
|
||||
from PIL import Image as Image
|
||||
from typing_extensions import Literal
|
||||
|
||||
from twisted.internet import defer
|
||||
from twisted.internet.defer import Deferred
|
||||
from twisted.test.proto_helpers import MemoryReactor
|
||||
|
||||
from synapse.api.errors import Codes
|
||||
from synapse.events import EventBase
|
||||
from synapse.events.spamcheck import load_legacy_spam_checkers
|
||||
from synapse.logging.context import make_deferred_yieldable
|
||||
@@ -572,11 +570,9 @@ class MediaRepoTests(unittest.HomeserverTestCase):
|
||||
)
|
||||
|
||||
|
||||
class TestSpamCheckerLegacy:
|
||||
class TestSpamChecker:
|
||||
"""A spam checker module that rejects all media that includes the bytes
|
||||
`evil`.
|
||||
|
||||
Uses the legacy Spam-Checker API.
|
||||
"""
|
||||
|
||||
def __init__(self, config: Dict[str, Any], api: ModuleApi) -> None:
|
||||
@@ -617,7 +613,7 @@ class TestSpamCheckerLegacy:
|
||||
return b"evil" in buf.getvalue()
|
||||
|
||||
|
||||
class SpamCheckerTestCaseLegacy(unittest.HomeserverTestCase):
|
||||
class SpamCheckerTestCase(unittest.HomeserverTestCase):
|
||||
servlets = [
|
||||
login.register_servlets,
|
||||
admin.register_servlets,
|
||||
@@ -641,8 +637,7 @@ class SpamCheckerTestCaseLegacy(unittest.HomeserverTestCase):
|
||||
{
|
||||
"spam_checker": [
|
||||
{
|
||||
"module": TestSpamCheckerLegacy.__module__
|
||||
+ ".TestSpamCheckerLegacy",
|
||||
"module": TestSpamChecker.__module__ + ".TestSpamChecker",
|
||||
"config": {},
|
||||
}
|
||||
]
|
||||
@@ -667,62 +662,3 @@ class SpamCheckerTestCaseLegacy(unittest.HomeserverTestCase):
|
||||
self.helper.upload_media(
|
||||
self.upload_resource, data, tok=self.tok, expect_code=400
|
||||
)
|
||||
|
||||
|
||||
EVIL_DATA = b"Some evil data"
|
||||
EVIL_DATA_EXPERIMENT = b"Some evil data to trigger the experimental tuple API"
|
||||
|
||||
|
||||
class SpamCheckerTestCase(unittest.HomeserverTestCase):
|
||||
servlets = [
|
||||
login.register_servlets,
|
||||
admin.register_servlets,
|
||||
]
|
||||
|
||||
def prepare(self, reactor: MemoryReactor, clock: Clock, hs: HomeServer) -> None:
|
||||
self.user = self.register_user("user", "pass")
|
||||
self.tok = self.login("user", "pass")
|
||||
|
||||
# Allow for uploading and downloading to/from the media repo
|
||||
self.media_repo = hs.get_media_repository_resource()
|
||||
self.download_resource = self.media_repo.children[b"download"]
|
||||
self.upload_resource = self.media_repo.children[b"upload"]
|
||||
|
||||
hs.get_module_api().register_spam_checker_callbacks(
|
||||
check_media_file_for_spam=self.check_media_file_for_spam
|
||||
)
|
||||
|
||||
async def check_media_file_for_spam(
|
||||
self, file_wrapper: ReadableFileWrapper, file_info: FileInfo
|
||||
) -> Union[Codes, Literal["NOT_SPAM"]]:
|
||||
buf = BytesIO()
|
||||
await file_wrapper.write_chunks_to(buf.write)
|
||||
|
||||
if buf.getvalue() == EVIL_DATA:
|
||||
return Codes.FORBIDDEN
|
||||
elif buf.getvalue() == EVIL_DATA_EXPERIMENT:
|
||||
return (Codes.FORBIDDEN, {})
|
||||
else:
|
||||
return "NOT_SPAM"
|
||||
|
||||
def test_upload_innocent(self) -> None:
|
||||
"""Attempt to upload some innocent data that should be allowed."""
|
||||
self.helper.upload_media(
|
||||
self.upload_resource, SMALL_PNG, tok=self.tok, expect_code=200
|
||||
)
|
||||
|
||||
def test_upload_ban(self) -> None:
|
||||
"""Attempt to upload some data that includes bytes "evil", which should
|
||||
get rejected by the spam checker.
|
||||
"""
|
||||
|
||||
self.helper.upload_media(
|
||||
self.upload_resource, EVIL_DATA, tok=self.tok, expect_code=400
|
||||
)
|
||||
|
||||
self.helper.upload_media(
|
||||
self.upload_resource,
|
||||
EVIL_DATA_EXPERIMENT,
|
||||
tok=self.tok,
|
||||
expect_code=400,
|
||||
)
|
||||
|
||||
@@ -196,13 +196,6 @@ class EventPushActionsStoreTestCase(HomeserverTestCase):
|
||||
_mark_read(10, 10)
|
||||
_assert_counts(0, 0)
|
||||
|
||||
_inject_actions(11, HIGHLIGHT)
|
||||
_assert_counts(1, 1)
|
||||
_mark_read(11, 11)
|
||||
_assert_counts(0, 0)
|
||||
_rotate(11)
|
||||
_assert_counts(0, 0)
|
||||
|
||||
def test_find_first_stream_ordering_after_ts(self) -> None:
|
||||
def add_event(so: int, ts: int) -> None:
|
||||
self.get_success(
|
||||
|
||||
Reference in New Issue
Block a user