mirror of
https://github.com/element-hq/synapse.git
synced 2025-12-11 01:40:27 +00:00
Compare commits
116 Commits
madlittlem
...
hughns/ind
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
98c27ab824 | ||
|
|
70d166e7b5 | ||
|
|
96425d4071 | ||
|
|
69e9b75373 | ||
|
|
5d0514f29b | ||
|
|
4e5410fdae | ||
|
|
12d65a6778 | ||
|
|
1006c12eb2 | ||
|
|
57efc8c03e | ||
|
|
a5e16a4ab5 | ||
|
|
80ad02e10e | ||
|
|
9512b84a72 | ||
|
|
22aa925523 | ||
|
|
0ab99369a1 | ||
|
|
6ececb8f2a | ||
|
|
2ce7a1edf7 | ||
|
|
ec885ffd33 | ||
|
|
11bc9a1b3a | ||
|
|
c5b379de66 | ||
|
|
adda2a4613 | ||
|
|
5d47138b46 | ||
|
|
d025b5ab50 | ||
|
|
ae6179b382 | ||
|
|
5dd6157972 | ||
|
|
1266138b66 | ||
|
|
24975eca4d | ||
|
|
451a9dc7b9 | ||
|
|
f6a3e5e1c2 | ||
|
|
05576f0b4b | ||
|
|
60aebdb27e | ||
|
|
b1b4b2944d | ||
|
|
bdcc9fa388 | ||
|
|
6a0c21fabd | ||
|
|
f40641c29b | ||
|
|
1bb528ee44 | ||
|
|
165f4ca776 | ||
|
|
475e192cbe | ||
|
|
43040a4051 | ||
|
|
b3e2d10f39 | ||
|
|
a5986ac229 | ||
|
|
006251a5d0 | ||
|
|
422f3ecec1 | ||
|
|
4e90221d87 | ||
|
|
e2610de208 | ||
|
|
e8c8924b81 | ||
|
|
e8e0f0fad7 | ||
|
|
beb7a951f4 | ||
|
|
d34f827ed8 | ||
|
|
9920417723 | ||
|
|
316d635906 | ||
|
|
8bbe66a9b9 | ||
|
|
d4e3ad04cd | ||
|
|
55c0391cc8 | ||
|
|
81e0f57800 | ||
|
|
ae4862c38f | ||
|
|
602956ef64 | ||
|
|
444b565c76 | ||
|
|
8068f31146 | ||
|
|
5210565c12 | ||
|
|
de955293cf | ||
|
|
93889eb2e7 | ||
|
|
ece66ba61c | ||
|
|
ef9ef99f59 | ||
|
|
cfbddc258f | ||
|
|
302534c348 | ||
|
|
f144b4c7e9 | ||
|
|
13dea6949b | ||
|
|
386cabda83 | ||
|
|
f53a3a56e2 | ||
|
|
2fc43e4219 | ||
|
|
b0d2aca164 | ||
|
|
f68e8d0021 | ||
|
|
89e7609f5c | ||
|
|
b89a66f831 | ||
|
|
b066b3aa04 | ||
|
|
e4b0cd87cc | ||
|
|
985b3ab58d | ||
|
|
afc3af7763 | ||
|
|
af2da0e47a | ||
|
|
ac8c9ac50d | ||
|
|
443a9eb335 | ||
|
|
aad26cb93f | ||
|
|
5173741c71 | ||
|
|
75e2c17d2a | ||
|
|
a851f6b237 | ||
|
|
c2e5e9e67c | ||
|
|
07a51d2a56 | ||
|
|
83fc225030 | ||
|
|
a9c0e27eb7 | ||
|
|
faf5b40520 | ||
|
|
af998e6c66 | ||
|
|
61b7c31772 | ||
|
|
3c8a116e1a | ||
|
|
51dd4df0a3 | ||
|
|
8881ad6d4b | ||
|
|
d40bc279ed | ||
|
|
d10872ee75 | ||
|
|
03937a1cae | ||
|
|
285de43e48 | ||
|
|
4900438712 | ||
|
|
cf982d2e32 | ||
|
|
7589565edd | ||
|
|
7ed23e072e | ||
|
|
4ac783549c | ||
|
|
1cb84aaab5 | ||
|
|
9b83fb7c16 | ||
|
|
c5b4be6d07 | ||
|
|
4c66a7cbed | ||
|
|
ebad618bf0 | ||
|
|
16af80b8fb | ||
|
|
e4a1f271b9 | ||
|
|
6b131a99fe | ||
|
|
76f7c91e44 | ||
|
|
b732d13d4c | ||
|
|
596b96411b | ||
|
|
f6c2b0ec2e |
@@ -53,7 +53,7 @@ if not IS_PR:
|
||||
"database": "sqlite",
|
||||
"extras": "all",
|
||||
}
|
||||
for version in ("3.9", "3.10", "3.11", "3.12")
|
||||
for version in ("3.9", "3.10", "3.11", "3.12", "3.13")
|
||||
)
|
||||
|
||||
trial_postgres_tests = [
|
||||
@@ -68,9 +68,9 @@ trial_postgres_tests = [
|
||||
if not IS_PR:
|
||||
trial_postgres_tests.append(
|
||||
{
|
||||
"python-version": "3.12",
|
||||
"python-version": "3.13",
|
||||
"database": "postgres",
|
||||
"postgres-version": "16",
|
||||
"postgres-version": "17",
|
||||
"extras": "all",
|
||||
}
|
||||
)
|
||||
|
||||
2
.github/workflows/docker.yml
vendored
2
.github/workflows/docker.yml
vendored
@@ -30,7 +30,7 @@ jobs:
|
||||
run: docker buildx inspect
|
||||
|
||||
- name: Install Cosign
|
||||
uses: sigstore/cosign-installer@v3.6.0
|
||||
uses: sigstore/cosign-installer@v3.7.0
|
||||
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
|
||||
143
CHANGES.md
143
CHANGES.md
@@ -1,3 +1,146 @@
|
||||
# Synapse 1.117.0 (2024-10-15)
|
||||
|
||||
No significant changes since 1.117.0rc1.
|
||||
|
||||
|
||||
|
||||
|
||||
# Synapse 1.117.0rc1 (2024-10-08)
|
||||
|
||||
### Features
|
||||
|
||||
- Add config option `redis.password_path`. ([\#17717](https://github.com/element-hq/synapse/issues/17717))
|
||||
|
||||
### Bugfixes
|
||||
|
||||
- Fix a rare bug introduced in v1.29.0 where invalidating a user's access token from a worker could raise an error. ([\#17779](https://github.com/element-hq/synapse/issues/17779))
|
||||
- In the response to `GET /_matrix/client/versions`, set the `unstable_features` flag for [MSC4140](https://github.com/matrix-org/matrix-spec-proposals/pull/4140) to `false` when server configuration disables support for delayed events. ([\#17780](https://github.com/element-hq/synapse/issues/17780))
|
||||
- Improve input validation and room membership checks in admin redaction API. ([\#17792](https://github.com/element-hq/synapse/issues/17792))
|
||||
|
||||
### Improved Documentation
|
||||
|
||||
- Clarify the docstring of `test_forget_when_not_left`. ([\#17628](https://github.com/element-hq/synapse/issues/17628))
|
||||
- Add documentation note about PYTHONMALLOC for accurate jemalloc memory tracking. Contributed by @hensg. ([\#17709](https://github.com/element-hq/synapse/issues/17709))
|
||||
- Remove spurious "TODO UPDATE ALL THIS" note in the Debian installation docs. ([\#17749](https://github.com/element-hq/synapse/issues/17749))
|
||||
- Explain how load balancing works for `federation_sender_instances`. ([\#17776](https://github.com/element-hq/synapse/issues/17776))
|
||||
|
||||
### Internal Changes
|
||||
|
||||
- Minor performance increase for large accounts using sliding sync. ([\#17751](https://github.com/element-hq/synapse/issues/17751))
|
||||
- Increase performance of the notifier when there are many syncing users. ([\#17765](https://github.com/element-hq/synapse/issues/17765), [\#17766](https://github.com/element-hq/synapse/issues/17766))
|
||||
- Fix performance of streams that don't change often. ([\#17767](https://github.com/element-hq/synapse/issues/17767))
|
||||
- Improve performance of sliding sync connections that do not ask for any rooms. ([\#17768](https://github.com/element-hq/synapse/issues/17768))
|
||||
- Reduce overhead of sliding sync E2EE loops. ([\#17771](https://github.com/element-hq/synapse/issues/17771))
|
||||
- Sliding sync minor performance speed up using new table. ([\#17787](https://github.com/element-hq/synapse/issues/17787))
|
||||
- Sliding sync minor performance improvement by omitting unchanged data from incremental responses. ([\#17788](https://github.com/element-hq/synapse/issues/17788))
|
||||
- Speed up sliding sync when there are many active subscriptions. ([\#17789](https://github.com/element-hq/synapse/issues/17789))
|
||||
- Add missing license headers on new source files. ([\#17799](https://github.com/element-hq/synapse/issues/17799))
|
||||
|
||||
|
||||
|
||||
### Updates to locked dependencies
|
||||
|
||||
* Bump phonenumbers from 8.13.45 to 8.13.46. ([\#17773](https://github.com/element-hq/synapse/issues/17773))
|
||||
* Bump python-multipart from 0.0.10 to 0.0.12. ([\#17772](https://github.com/element-hq/synapse/issues/17772))
|
||||
* Bump regex from 1.10.6 to 1.11.0. ([\#17770](https://github.com/element-hq/synapse/issues/17770))
|
||||
* Bump ruff from 0.6.7 to 0.6.8. ([\#17774](https://github.com/element-hq/synapse/issues/17774))
|
||||
|
||||
# Synapse 1.116.0 (2024-10-01)
|
||||
|
||||
No significant changes since 1.116.0rc2.
|
||||
|
||||
|
||||
|
||||
|
||||
# Synapse 1.116.0rc2 (2024-09-26)
|
||||
|
||||
### Features
|
||||
|
||||
- Add implementation of restricting who can overwrite a state event as proposed by [MSC3757](https://github.com/matrix-org/matrix-spec-proposals/pull/3757). ([\#17513](https://github.com/element-hq/synapse/issues/17513))
|
||||
|
||||
|
||||
|
||||
|
||||
# Synapse 1.116.0rc1 (2024-09-25)
|
||||
|
||||
### Features
|
||||
|
||||
- Add initial implementation of delayed events as proposed by [MSC4140](https://github.com/matrix-org/matrix-spec-proposals/pull/4140). ([\#17326](https://github.com/element-hq/synapse/issues/17326))
|
||||
- Add an asynchronous Admin API endpoint [to redact all a user's events](https://element-hq.github.io/synapse/v1.116/admin_api/user_admin_api.html#redact-all-the-events-of-a-user),
|
||||
and [an endpoint to check on the status of that redaction task](https://element-hq.github.io/synapse/v1.116/admin_api/user_admin_api.html#check-the-status-of-a-redaction-process). ([\#17506](https://github.com/element-hq/synapse/issues/17506))
|
||||
- Add support for the `tags` and `not_tags` filters for [MSC4186](https://github.com/matrix-org/matrix-spec-proposals/pull/4186) Sliding Sync. ([\#17662](https://github.com/element-hq/synapse/issues/17662))
|
||||
- Guests can use the new media endpoints to download media, as described by [MSC4189](https://github.com/matrix-org/matrix-spec-proposals/pull/4189). ([\#17675](https://github.com/element-hq/synapse/issues/17675))
|
||||
- Add config option `turn_shared_secret_path`. ([\#17690](https://github.com/element-hq/synapse/issues/17690))
|
||||
- Return room tags in [MSC4186](https://github.com/matrix-org/matrix-spec-proposals/pull/4186) Sliding Sync account data extension. ([\#17707](https://github.com/element-hq/synapse/issues/17707))
|
||||
|
||||
### Bugfixes
|
||||
|
||||
- Make sure we get up-to-date state information when using the new [MSC4186](https://github.com/matrix-org/matrix-spec-proposals/pull/4186) Sliding Sync tables to derive room membership. ([\#17692](https://github.com/element-hq/synapse/issues/17692))
|
||||
- Fix bug where room account data would not correctly be sent down [MSC4186](https://github.com/matrix-org/matrix-spec-proposals/pull/4186) Sliding Sync for old rooms. ([\#17695](https://github.com/element-hq/synapse/issues/17695))
|
||||
- Fix a bug in [MSC4186](https://github.com/matrix-org/matrix-spec-proposals/pull/4186) Sliding Sync which could prevent /sync from working for certain user accounts. ([\#17727](https://github.com/element-hq/synapse/issues/17727), [\#17733](https://github.com/element-hq/synapse/issues/17733))
|
||||
- Ignore invites from ignored users in Sliding Sync. ([\#17729](https://github.com/element-hq/synapse/issues/17729))
|
||||
- Fix bug in [MSC4186](https://github.com/matrix-org/matrix-spec-proposals/pull/4186) Sliding Sync where the server would incorrectly return a negative bump stamp, which caused Element X apps to stop syncing. ([\#17748](https://github.com/element-hq/synapse/issues/17748))
|
||||
|
||||
### Internal Changes
|
||||
|
||||
- Import pydantic objects from the `_pydantic_compat` module.
|
||||
This allows `check_pydantic_models.py` to mock those pydantic objects
|
||||
only in the synapse module, and not interfere with pydantic objects in
|
||||
external dependencies. ([\#17667](https://github.com/element-hq/synapse/issues/17667))
|
||||
- Use [MSC4186](https://github.com/matrix-org/matrix-spec-proposals/pull/4186) Sliding Sync tables as a bulk shortcut for getting the max `event_stream_ordering` of rooms. ([\#17693](https://github.com/element-hq/synapse/issues/17693))
|
||||
- Speed up [MSC4186](https://github.com/matrix-org/matrix-spec-proposals/pull/4186) sliding sync requests a bit where there are many room changes. ([\#17696](https://github.com/element-hq/synapse/issues/17696))
|
||||
- Refactor [MSC4186](https://github.com/matrix-org/matrix-spec-proposals/pull/4186) sliding sync filter unit tests so the sliding sync API has better test coverage. ([\#17703](https://github.com/element-hq/synapse/issues/17703))
|
||||
- Fetch `bump_stamp`s more efficiently in [MSC4186](https://github.com/matrix-org/matrix-spec-proposals/pull/4186) Sliding Sync. ([\#17723](https://github.com/element-hq/synapse/issues/17723))
|
||||
- Shortcut for checking if certain background updates have completed (utilized in [MSC4186](https://github.com/matrix-org/matrix-spec-proposals/pull/4186) Sliding Sync). ([\#17724](https://github.com/element-hq/synapse/issues/17724))
|
||||
- More efficiently fetch rooms for [MSC4186](https://github.com/matrix-org/matrix-spec-proposals/pull/4186) Sliding Sync. ([\#17725](https://github.com/element-hq/synapse/issues/17725))
|
||||
- Fix `_bulk_get_max_event_pos` being inefficient. ([\#17728](https://github.com/element-hq/synapse/issues/17728))
|
||||
- Add cache to `get_tags_for_room(...)`. ([\#17730](https://github.com/element-hq/synapse/issues/17730))
|
||||
- Small performance improvement in speeding up [MSC4186](https://github.com/matrix-org/matrix-spec-proposals/pull/4186) Sliding Sync. ([\#17731](https://github.com/element-hq/synapse/issues/17731))
|
||||
- Minor speed up of initial [MSC4186](https://github.com/matrix-org/matrix-spec-proposals/pull/4186) sliding sync requests. ([\#17734](https://github.com/element-hq/synapse/issues/17734))
|
||||
- Remove usage of the deprecated `cgi` module, deprecated in Python 3.11 and removed in Python 3.13. ([\#17741](https://github.com/element-hq/synapse/issues/17741))
|
||||
- Fix typing of a variable that is not `Unknown` anymore after updating `treq`. ([\#17744](https://github.com/element-hq/synapse/issues/17744))
|
||||
|
||||
|
||||
|
||||
### Updates to locked dependencies
|
||||
|
||||
* Bump anyhow from 1.0.86 to 1.0.89. ([\#17685](https://github.com/element-hq/synapse/issues/17685), [\#17716](https://github.com/element-hq/synapse/issues/17716))
|
||||
* Bump bytes from 1.7.1 to 1.7.2. ([\#17743](https://github.com/element-hq/synapse/issues/17743))
|
||||
* Bump cryptography from 43.0.0 to 43.0.1. ([\#17689](https://github.com/element-hq/synapse/issues/17689))
|
||||
* Bump idna from 3.8 to 3.10. ([\#17758](https://github.com/element-hq/synapse/issues/17758))
|
||||
* Bump msgpack from 1.0.8 to 1.1.0. ([\#17759](https://github.com/element-hq/synapse/issues/17759))
|
||||
* Bump phonenumbers from 8.13.44 to 8.13.45. ([\#17762](https://github.com/element-hq/synapse/issues/17762))
|
||||
* Bump prometheus-client from 0.20.0 to 0.21.0. ([\#17746](https://github.com/element-hq/synapse/issues/17746))
|
||||
* Bump pyasn1 from 0.6.0 to 0.6.1. ([\#17714](https://github.com/element-hq/synapse/issues/17714))
|
||||
* Bump pyasn1-modules from 0.4.0 to 0.4.1. ([\#17747](https://github.com/element-hq/synapse/issues/17747))
|
||||
* Bump pydantic from 2.8.2 to 2.9.2. ([\#17756](https://github.com/element-hq/synapse/issues/17756))
|
||||
* Bump python-multipart from 0.0.9 to 0.0.10. ([\#17745](https://github.com/element-hq/synapse/issues/17745))
|
||||
* Bump ruff from 0.6.4 to 0.6.7. ([\#17715](https://github.com/element-hq/synapse/issues/17715), [\#17760](https://github.com/element-hq/synapse/issues/17760))
|
||||
* Bump sentry-sdk from 2.13.0 to 2.14.0. ([\#17712](https://github.com/element-hq/synapse/issues/17712))
|
||||
* Bump serde from 1.0.209 to 1.0.210. ([\#17686](https://github.com/element-hq/synapse/issues/17686))
|
||||
* Bump serde_json from 1.0.127 to 1.0.128. ([\#17687](https://github.com/element-hq/synapse/issues/17687))
|
||||
* Bump treq from 23.11.0 to 24.9.1. ([\#17744](https://github.com/element-hq/synapse/issues/17744))
|
||||
* Bump types-pyyaml from 6.0.12.20240808 to 6.0.12.20240917. ([\#17755](https://github.com/element-hq/synapse/issues/17755))
|
||||
* Bump types-requests from 2.32.0.20240712 to 2.32.0.20240914. ([\#17713](https://github.com/element-hq/synapse/issues/17713))
|
||||
* Bump types-setuptools from 74.1.0.20240907 to 75.1.0.20240917. ([\#17757](https://github.com/element-hq/synapse/issues/17757))
|
||||
|
||||
# Synapse 1.115.0 (2024-09-17)
|
||||
|
||||
No significant changes since 1.115.0rc2.
|
||||
|
||||
|
||||
|
||||
|
||||
# Synapse 1.115.0rc2 (2024-09-12)
|
||||
|
||||
### Internal Changes
|
||||
|
||||
- Pre-populate room data used in experimental [MSC3575](https://github.com/matrix-org/matrix-spec-proposals/pull/3575) Sliding Sync `/sync` endpoint for quick filtering/sorting. ([\#17652](https://github.com/element-hq/synapse/issues/17652))
|
||||
- Speed up sliding sync by reducing amount of data pulled out of the database for large rooms. ([\#17683](https://github.com/element-hq/synapse/issues/17683))
|
||||
|
||||
|
||||
|
||||
|
||||
# Synapse 1.115.0rc1 (2024-09-10)
|
||||
|
||||
### Features
|
||||
|
||||
24
Cargo.lock
generated
24
Cargo.lock
generated
@@ -13,9 +13,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "anyhow"
|
||||
version = "1.0.87"
|
||||
version = "1.0.90"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "10f00e1f6e58a40e807377c75c6a7f97bf9044fab57816f2414e6f5f4499d7b8"
|
||||
checksum = "37bf3594c4c988a53154954629820791dde498571819ae4ca50ca811e060cc95"
|
||||
|
||||
[[package]]
|
||||
name = "arc-swap"
|
||||
@@ -67,9 +67,9 @@ checksum = "79296716171880943b8470b5f8d03aa55eb2e645a4874bdbb28adb49162e012c"
|
||||
|
||||
[[package]]
|
||||
name = "bytes"
|
||||
version = "1.7.1"
|
||||
version = "1.7.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8318a53db07bb3f8dca91a600466bdb3f2eaadeedfdbcf02e1accbad9271ba50"
|
||||
checksum = "428d9aa8fbc0670b7b8d6030a7fadd0f86151cae55e4dbbece15f3780a3dfaf3"
|
||||
|
||||
[[package]]
|
||||
name = "cfg-if"
|
||||
@@ -444,9 +444,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "regex"
|
||||
version = "1.10.6"
|
||||
version = "1.11.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4219d74c6b67a3654a9fbebc4b419e22126d13d2f3c4a07ee0cb61ff79a79619"
|
||||
checksum = "38200e5ee88914975b69f657f0801b6f6dccafd44fd9326302a4aaeecfacb1d8"
|
||||
dependencies = [
|
||||
"aho-corasick",
|
||||
"memchr",
|
||||
@@ -456,9 +456,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "regex-automata"
|
||||
version = "0.4.6"
|
||||
version = "0.4.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "86b83b8b9847f9bf95ef68afb0b8e6cdb80f498442f5179a29fad448fcc1eaea"
|
||||
checksum = "368758f23274712b504848e9d5a6f010445cc8b87a7cdb4d7cbee666c1288da3"
|
||||
dependencies = [
|
||||
"aho-corasick",
|
||||
"memchr",
|
||||
@@ -467,9 +467,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "regex-syntax"
|
||||
version = "0.8.3"
|
||||
version = "0.8.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "adad44e29e4c806119491a7f06f03de4d1af22c3a680dd47f1e6e179439d1f56"
|
||||
checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c"
|
||||
|
||||
[[package]]
|
||||
name = "ryu"
|
||||
@@ -505,9 +505,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "serde_json"
|
||||
version = "1.0.128"
|
||||
version = "1.0.132"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6ff5456707a1de34e7e37f2a6fd3d3f808c318259cbd01ab6377795054b483d8"
|
||||
checksum = "d726bfaff4b320266d395898905d0eba0345aae23b54aee3a737e260fd46db03"
|
||||
dependencies = [
|
||||
"itoa",
|
||||
"memchr",
|
||||
|
||||
1
changelog.d/17627.doc
Normal file
1
changelog.d/17627.doc
Normal file
@@ -0,0 +1 @@
|
||||
Clarify when the `user_may_invite` and `user_may_send_3pid_invite` module callbacks are called.
|
||||
@@ -1 +0,0 @@
|
||||
Guests can use the new media endpoints to download media, as described by [MSC4189](https://github.com/matrix-org/matrix-spec-proposals/pull/4189).
|
||||
@@ -1 +0,0 @@
|
||||
Add config option `turn_shared_secret_path`.
|
||||
@@ -1 +0,0 @@
|
||||
Use Sliding Sync tables as a bulk shortcut for getting the max `event_stream_ordering` of rooms.
|
||||
1
changelog.d/17708.feature
Normal file
1
changelog.d/17708.feature
Normal file
@@ -0,0 +1 @@
|
||||
Added the `display_name_claim` option to the JWT configuration. This option allows specifying the claim key that contains the user's display name in the JWT payload.
|
||||
1
changelog.d/17718.misc
Normal file
1
changelog.d/17718.misc
Normal file
@@ -0,0 +1 @@
|
||||
Slight optimization when fetching state/events for Sliding Sync.
|
||||
1
changelog.d/17736.bugfix
Normal file
1
changelog.d/17736.bugfix
Normal file
@@ -0,0 +1 @@
|
||||
Fix saving of PNG thumbnails, when the original image is in the CMYK color space.
|
||||
1
changelog.d/17752.misc
Normal file
1
changelog.d/17752.misc
Normal file
@@ -0,0 +1 @@
|
||||
Add Python 3.13 and Postgres 17 to the test matrix.
|
||||
1
changelog.d/17783.feature
Normal file
1
changelog.d/17783.feature
Normal file
@@ -0,0 +1 @@
|
||||
Implement [MSC4210](https://github.com/matrix-org/matrix-spec-proposals/pull/4210): Remove legacy mentions. Contributed by @tulir @ Beeper.
|
||||
1
changelog.d/17785.bugfix
Normal file
1
changelog.d/17785.bugfix
Normal file
@@ -0,0 +1 @@
|
||||
Fix bug with sliding sync where the server would not return state that was added to the `required_state` config.
|
||||
1
changelog.d/17786.misc
Normal file
1
changelog.d/17786.misc
Normal file
@@ -0,0 +1 @@
|
||||
Add a test for downloading and thumbnailing a CMYK JPEG.
|
||||
1
changelog.d/17802.doc
Normal file
1
changelog.d/17802.doc
Normal file
@@ -0,0 +1 @@
|
||||
Correct documentation to refer to the `--config-path` argument instead of `--config-file`.
|
||||
1
changelog.d/17803.misc
Normal file
1
changelog.d/17803.misc
Normal file
@@ -0,0 +1 @@
|
||||
Test github token before running release script steps.
|
||||
1
changelog.d/17805.bugfix
Normal file
1
changelog.d/17805.bugfix
Normal file
@@ -0,0 +1 @@
|
||||
Fix bug with sliding sync where the server would not return state that was added to the `required_state` config.
|
||||
1
changelog.d/17824.misc
Normal file
1
changelog.d/17824.misc
Normal file
@@ -0,0 +1 @@
|
||||
Build debian packages for new Ubuntu versions, and stop building for no longer supported versions.
|
||||
1
changelog.d/17825.doc
Normal file
1
changelog.d/17825.doc
Normal file
@@ -0,0 +1 @@
|
||||
Fix typo in `target_cache_memory_usage` docs.
|
||||
1
changelog.d/17826.misc
Normal file
1
changelog.d/17826.misc
Normal file
@@ -0,0 +1 @@
|
||||
Enable the `.org.matrix.msc4028.encrypted_event` push rule by default in accordance with [MSC4028](https://github.com/matrix-org/matrix-spec-proposals/pull/4028). Note that the corresponding experimental feature must still be switched on for this push rule to have any effect.
|
||||
1
changelog.d/17835.bugfix
Normal file
1
changelog.d/17835.bugfix
Normal file
@@ -0,0 +1 @@
|
||||
Fix a bug in [MSC4186](https://github.com/matrix-org/matrix-spec-proposals/pull/4186) Sliding Sync that would cause rooms to stay forgotten and hidden even after rejoining.
|
||||
1
changelog.d/17842.misc
Normal file
1
changelog.d/17842.misc
Normal file
@@ -0,0 +1 @@
|
||||
Fix some typing issues uncovered by upgrading mypy to 1.11.x.
|
||||
@@ -20,8 +20,8 @@
|
||||
#
|
||||
|
||||
import argparse
|
||||
import cgi
|
||||
import datetime
|
||||
import html
|
||||
import json
|
||||
import urllib.request
|
||||
from typing import List
|
||||
@@ -85,7 +85,7 @@ def make_graph(pdus: List[dict], filename_prefix: str) -> None:
|
||||
"name": name,
|
||||
"type": pdu.get("pdu_type"),
|
||||
"state_key": pdu.get("state_key"),
|
||||
"content": cgi.escape(json.dumps(pdu.get("content")), quote=True),
|
||||
"content": html.escape(json.dumps(pdu.get("content")), quote=True),
|
||||
"time": t,
|
||||
"depth": pdu.get("depth"),
|
||||
}
|
||||
|
||||
42
debian/changelog
vendored
42
debian/changelog
vendored
@@ -1,3 +1,45 @@
|
||||
matrix-synapse-py3 (1.117.0) stable; urgency=medium
|
||||
|
||||
* New Synapse release 1.117.0.
|
||||
|
||||
-- Synapse Packaging team <packages@matrix.org> Tue, 15 Oct 2024 10:46:30 +0100
|
||||
|
||||
matrix-synapse-py3 (1.117.0~rc1) stable; urgency=medium
|
||||
|
||||
* New Synapse release 1.117.0rc1.
|
||||
|
||||
-- Synapse Packaging team <packages@matrix.org> Tue, 08 Oct 2024 14:37:11 +0100
|
||||
|
||||
matrix-synapse-py3 (1.116.0) stable; urgency=medium
|
||||
|
||||
* New Synapse release 1.116.0.
|
||||
|
||||
-- Synapse Packaging team <packages@matrix.org> Tue, 01 Oct 2024 11:14:07 +0100
|
||||
|
||||
matrix-synapse-py3 (1.116.0~rc2) stable; urgency=medium
|
||||
|
||||
* New synapse release 1.116.0rc2.
|
||||
|
||||
-- Synapse Packaging team <packages@matrix.org> Thu, 26 Sep 2024 13:28:43 +0000
|
||||
|
||||
matrix-synapse-py3 (1.116.0~rc1) stable; urgency=medium
|
||||
|
||||
* New synapse release 1.116.0rc1.
|
||||
|
||||
-- Synapse Packaging team <packages@matrix.org> Wed, 25 Sep 2024 09:34:07 +0000
|
||||
|
||||
matrix-synapse-py3 (1.115.0) stable; urgency=medium
|
||||
|
||||
* New Synapse release 1.115.0.
|
||||
|
||||
-- Synapse Packaging team <packages@matrix.org> Tue, 17 Sep 2024 14:32:10 +0100
|
||||
|
||||
matrix-synapse-py3 (1.115.0~rc2) stable; urgency=medium
|
||||
|
||||
* New Synapse release 1.115.0rc2.
|
||||
|
||||
-- Synapse Packaging team <packages@matrix.org> Thu, 12 Sep 2024 11:10:15 +0100
|
||||
|
||||
matrix-synapse-py3 (1.115.0~rc1) stable; urgency=medium
|
||||
|
||||
* New Synapse release 1.115.0rc1.
|
||||
|
||||
@@ -111,6 +111,9 @@ server_notices:
|
||||
system_mxid_avatar_url: ""
|
||||
room_name: "Server Alert"
|
||||
|
||||
# Enable delayed events (msc4140)
|
||||
max_event_delay_duration: 24h
|
||||
|
||||
|
||||
# Disable sync cache so that initial `/sync` requests are up-to-date.
|
||||
caches:
|
||||
|
||||
@@ -1361,3 +1361,83 @@ Returns a `404` HTTP status code if no user was found, with a response body like
|
||||
```
|
||||
|
||||
_Added in Synapse 1.72.0._
|
||||
|
||||
|
||||
## Redact all the events of a user
|
||||
|
||||
The API is
|
||||
```
|
||||
POST /_synapse/admin/v1/user/$user_id/redact
|
||||
|
||||
{
|
||||
"rooms": ["!roomid1", "!roomid2"]
|
||||
}
|
||||
```
|
||||
If an empty list is provided as the key for `rooms`, all events in all the rooms the user is member of will be redacted,
|
||||
otherwise all the events in the rooms provided in the request will be redacted.
|
||||
|
||||
The API starts redaction process running, and returns immediately with a JSON body with
|
||||
a redact id which can be used to query the status of the redaction process:
|
||||
|
||||
```json
|
||||
{
|
||||
"redact_id": "<opaque id>"
|
||||
}
|
||||
```
|
||||
|
||||
**Parameters**
|
||||
|
||||
The following parameters should be set in the URL:
|
||||
|
||||
- `user_id` - The fully qualified MXID of the user: for example, `@user:server.com`.
|
||||
|
||||
The following JSON body parameter must be provided:
|
||||
|
||||
- `rooms` - A list of rooms to redact the user's events in. If an empty list is provided all events in all rooms
|
||||
the user is a member of will be redacted
|
||||
|
||||
_Added in Synapse 1.116.0._
|
||||
|
||||
The following JSON body parameters are optional:
|
||||
|
||||
- `reason` - Reason the redaction is being requested, ie "spam", "abuse", etc. This will be included in each redaction event, and be visible to users.
|
||||
- `limit` - a limit on the number of the user's events to search for ones that can be redacted (events are redacted newest to oldest) in each room, defaults to 1000 if not provided
|
||||
|
||||
|
||||
## Check the status of a redaction process
|
||||
|
||||
It is possible to query the status of the background task for redacting a user's events.
|
||||
The status can be queried up to 24 hours after completion of the task,
|
||||
or until Synapse is restarted (whichever happens first).
|
||||
|
||||
The API is:
|
||||
|
||||
```
|
||||
GET /_synapse/admin/v1/user/redact_status/$redact_id
|
||||
```
|
||||
|
||||
A response body like the following is returned:
|
||||
|
||||
```
|
||||
{
|
||||
"status": "active",
|
||||
"failed_redactions": [],
|
||||
}
|
||||
```
|
||||
|
||||
**Parameters**
|
||||
|
||||
The following parameters should be set in the URL:
|
||||
|
||||
* `redact_id` - string - The ID for this redaction process, provided when the redaction was requested.
|
||||
|
||||
|
||||
**Response**
|
||||
|
||||
The following fields are returned in the JSON response body:
|
||||
|
||||
- `status` - string - one of scheduled/active/completed/failed, indicating the status of the redaction job
|
||||
- `failed_redactions` - dictionary - the keys of the dict are event ids the process was unable to redact, if any, and the values are
|
||||
the corresponding error that caused the redaction to fail
|
||||
|
||||
_Added in Synapse 1.116.0._
|
||||
@@ -76,8 +76,9 @@ _Changed in Synapse v1.62.0: `synapse.module_api.NOT_SPAM` and `synapse.module_a
|
||||
async def user_may_invite(inviter: str, invitee: str, room_id: str) -> Union["synapse.module_api.NOT_SPAM", "synapse.module_api.errors.Codes", bool]
|
||||
```
|
||||
|
||||
Called when processing an invitation. Both inviter and invitee are
|
||||
represented by their Matrix user ID (e.g. `@alice:example.com`).
|
||||
Called when processing an invitation, both when one is created locally or when
|
||||
receiving an invite over federation. Both inviter and invitee are represented by
|
||||
their Matrix user ID (e.g. `@alice:example.com`).
|
||||
|
||||
|
||||
The callback must return one of:
|
||||
@@ -112,7 +113,9 @@ async def user_may_send_3pid_invite(
|
||||
```
|
||||
|
||||
Called when processing an invitation using a third-party identifier (also called a 3PID,
|
||||
e.g. an email address or a phone number).
|
||||
e.g. an email address or a phone number). It is only called when a 3PID invite is created
|
||||
locally - not when one is received in a room over federation. If the 3PID is already associated
|
||||
with a Matrix ID, the spam check will go through the `user_may_invite` callback instead.
|
||||
|
||||
The inviter is represented by their Matrix user ID (e.g. `@alice:example.com`), and the
|
||||
invitee is represented by its medium (e.g. "email") and its address
|
||||
|
||||
@@ -52,8 +52,6 @@ architecture via <https://packages.matrix.org/debian/>.
|
||||
|
||||
To install the latest release:
|
||||
|
||||
TODO UPDATE ALL THIS
|
||||
|
||||
```sh
|
||||
sudo apt install -y lsb-release wget apt-transport-https
|
||||
sudo wget -O /usr/share/keyrings/matrix-org-archive-keyring.gpg https://packages.matrix.org/debian/matrix-org-archive-keyring.gpg
|
||||
@@ -316,7 +314,7 @@ sudo dnf group install "Development Tools"
|
||||
|
||||
*Note: The term "RHEL" below refers to both Red Hat Enterprise Linux and Rocky Linux. The distributions are 1:1 binary compatible.*
|
||||
|
||||
It's recommended to use the latest Python versions.
|
||||
It's recommended to use the latest Python versions.
|
||||
|
||||
RHEL 8 in particular ships with Python 3.6 by default which is EOL and therefore no longer supported by Synapse. RHEL 9 ship with Python 3.9 which is still supported by the Python core team as of this writing. However, newer Python versions provide significant performance improvements and they're available in official distributions' repositories. Therefore it's recommended to use them.
|
||||
|
||||
@@ -346,7 +344,7 @@ dnf install python3.12 python3.12-devel
|
||||
```
|
||||
Finally, install common prerequisites
|
||||
```bash
|
||||
dnf install libicu libicu-devel libpq5 libpq5-devel lz4 pkgconf
|
||||
dnf install libicu libicu-devel libpq5 libpq5-devel lz4 pkgconf
|
||||
dnf group install "Development Tools"
|
||||
```
|
||||
###### Using venv module instead of virtualenv command
|
||||
@@ -355,7 +353,7 @@ It's recommended to use Python venv module directly rather than the virtualenv c
|
||||
* On RHEL 9, virtualenv is only available on [EPEL](https://docs.fedoraproject.org/en-US/epel/).
|
||||
* On RHEL 8, virtualenv is based on Python 3.6. It does not support creating 3.11/3.12 virtual environments.
|
||||
|
||||
Here's an example of creating Python 3.12 virtual environment and installing Synapse from PyPI.
|
||||
Here's an example of creating Python 3.12 virtual environment and installing Synapse from PyPI.
|
||||
|
||||
```bash
|
||||
mkdir -p ~/synapse
|
||||
|
||||
@@ -255,6 +255,8 @@ line to `/etc/default/matrix-synapse`:
|
||||
|
||||
LD_PRELOAD=/usr/lib/x86_64-linux-gnu/libjemalloc.so.2
|
||||
|
||||
*Note*: You may need to set `PYTHONMALLOC=malloc` to ensure that `jemalloc` can accurately calculate memory usage. By default, Python uses its internal small-object allocator, which may interfere with jemalloc's ability to track memory consumption correctly. This could prevent the [cache_autotuning](../configuration/config_documentation.md#caches-and-associated-values) feature from functioning as expected, as the Python allocator may not reach the memory threshold set by `max_cache_memory_usage`, thus not triggering the cache eviction process.
|
||||
|
||||
This made a significant difference on Python 2.7 - it's unclear how
|
||||
much of an improvement it provides on Python 3.x.
|
||||
|
||||
|
||||
@@ -761,6 +761,19 @@ email:
|
||||
password_reset: "[%(server_name)s] Password reset"
|
||||
email_validation: "[%(server_name)s] Validate your email"
|
||||
```
|
||||
---
|
||||
### `max_event_delay_duration`
|
||||
|
||||
The maximum allowed duration by which sent events can be delayed, as per
|
||||
[MSC4140](https://github.com/matrix-org/matrix-spec-proposals/pull/4140).
|
||||
Must be a positive value if set.
|
||||
|
||||
Defaults to no duration (`null`), which disallows sending delayed events.
|
||||
|
||||
Example configuration:
|
||||
```yaml
|
||||
max_event_delay_duration: 24h
|
||||
```
|
||||
|
||||
## Homeserver blocking
|
||||
Useful options for Synapse admins.
|
||||
@@ -1421,7 +1434,7 @@ number of entries that can be stored.
|
||||
Please see the [Config Conventions](#config-conventions) for information on how to specify memory size and cache expiry
|
||||
durations.
|
||||
* `max_cache_memory_usage` sets a ceiling on how much memory the cache can use before caches begin to be continuously evicted.
|
||||
They will continue to be evicted until the memory usage drops below the `target_memory_usage`, set in
|
||||
They will continue to be evicted until the memory usage drops below the `target_cache_memory_usage`, set in
|
||||
the setting below, or until the `min_cache_ttl` is hit. There is no default value for this option.
|
||||
* `target_cache_memory_usage` sets a rough target for the desired memory usage of the caches. There is no default value
|
||||
for this option.
|
||||
@@ -3709,6 +3722,8 @@ Additional sub-options for this setting include:
|
||||
Required if `enabled` is set to true.
|
||||
* `subject_claim`: Name of the claim containing a unique identifier for the user.
|
||||
Optional, defaults to `sub`.
|
||||
* `display_name_claim`: Name of the claim containing the display name for the user. Optional.
|
||||
If provided, the display name will be set to the value of this claim upon first login.
|
||||
* `issuer`: The issuer to validate the "iss" claim against. Optional. If provided the
|
||||
"iss" claim will be required and validated for all JSON web tokens.
|
||||
* `audiences`: A list of audiences to validate the "aud" claim against. Optional.
|
||||
@@ -3723,6 +3738,7 @@ jwt_config:
|
||||
secret: "provided-by-your-issuer"
|
||||
algorithm: "provided-by-your-issuer"
|
||||
subject_claim: "name_of_claim"
|
||||
display_name_claim: "name_of_claim"
|
||||
issuer: "provided-by-your-issuer"
|
||||
audiences:
|
||||
- "provided-by-your-issuer"
|
||||
@@ -4355,7 +4371,13 @@ It is possible to scale the processes that handle sending outbound federation re
|
||||
by running a [`generic_worker`](../../workers.md#synapseappgeneric_worker) and adding it's [`worker_name`](#worker_name) to
|
||||
a `federation_sender_instances` map. Doing so will remove handling of this function from
|
||||
the main process. Multiple workers can be added to this map, in which case the work is
|
||||
balanced across them.
|
||||
balanced across them.
|
||||
|
||||
The way that the load balancing works is any outbound federation request will be assigned
|
||||
to a federation sender worker based on the hash of the destination server name. This
|
||||
means that all requests being sent to the same destination will be processed by the same
|
||||
worker instance. Multiple `federation_sender_instances` are useful if there is a federation
|
||||
with multiple servers.
|
||||
|
||||
This configuration setting must be shared between all workers handling federation
|
||||
sending, and if changed all federation sender workers must be stopped at the same time
|
||||
@@ -4505,6 +4527,9 @@ This setting has the following sub-options:
|
||||
* `path`: The full path to a local Unix socket file. **If this is used, `host` and
|
||||
`port` are ignored.** Defaults to `/tmp/redis.sock'
|
||||
* `password`: Optional password if configured on the Redis instance.
|
||||
* `password_path`: Alternative to `password`, reading the password from an
|
||||
external file. The file should be a plain text file, containing only the
|
||||
password. Synapse reads the password from the given file once at startup.
|
||||
* `dbid`: Optional redis dbid if needs to connect to specific redis logical db.
|
||||
* `use_tls`: Whether to use tls connection. Defaults to false.
|
||||
* `certificate_file`: Optional path to the certificate file
|
||||
@@ -4518,13 +4543,16 @@ This setting has the following sub-options:
|
||||
|
||||
_Changed in Synapse 1.85.0: Added path option to use a local Unix socket_
|
||||
|
||||
_Changed in Synapse 1.116.0: Added password\_path_
|
||||
|
||||
Example configuration:
|
||||
```yaml
|
||||
redis:
|
||||
enabled: true
|
||||
host: localhost
|
||||
port: 6379
|
||||
password: <secret_password>
|
||||
password_path: <path_to_the_password_file>
|
||||
# OR password: <secret_password>
|
||||
dbid: <dbid>
|
||||
#use_tls: True
|
||||
#certificate_file: <path_to_the_certificate_file>
|
||||
|
||||
@@ -177,11 +177,11 @@ The following applies to Synapse installations that have been installed from sou
|
||||
|
||||
You can start the main Synapse process with Poetry by running the following command:
|
||||
```console
|
||||
poetry run synapse_homeserver --config-file [your homeserver.yaml]
|
||||
poetry run synapse_homeserver --config-path [your homeserver.yaml]
|
||||
```
|
||||
For worker setups, you can run the following command
|
||||
```console
|
||||
poetry run synapse_worker --config-file [your homeserver.yaml] --config-file [your worker.yaml]
|
||||
poetry run synapse_worker --config-path [your homeserver.yaml] --config-path [your worker.yaml]
|
||||
```
|
||||
## Available worker applications
|
||||
|
||||
@@ -290,6 +290,7 @@ information.
|
||||
Additionally, the following REST endpoints can be handled for GET requests:
|
||||
|
||||
^/_matrix/client/(api/v1|r0|v3|unstable)/pushrules/
|
||||
^/_matrix/client/unstable/org.matrix.msc4140/delayed_events
|
||||
|
||||
Pagination requests can also be handled, but all requests for a given
|
||||
room must be routed to the same instance. Additionally, care must be taken to
|
||||
|
||||
812
poetry.lock
generated
812
poetry.lock
generated
File diff suppressed because it is too large
Load Diff
@@ -97,7 +97,7 @@ module-name = "synapse.synapse_rust"
|
||||
|
||||
[tool.poetry]
|
||||
name = "matrix-synapse"
|
||||
version = "1.115.0rc1"
|
||||
version = "1.117.0"
|
||||
description = "Homeserver for the Matrix decentralised comms protocol"
|
||||
authors = ["Matrix.org Team and Contributors <packages@matrix.org>"]
|
||||
license = "AGPL-3.0-or-later"
|
||||
@@ -320,7 +320,7 @@ all = [
|
||||
# failing on new releases. Keeping lower bounds loose here means that dependabot
|
||||
# can bump versions without having to update the content-hash in the lockfile.
|
||||
# This helps prevents merge conflicts when running a batch of dependabot updates.
|
||||
ruff = "0.6.4"
|
||||
ruff = "0.6.9"
|
||||
# Type checking only works with the pydantic.v1 compat module from pydantic v2
|
||||
pydantic = "^2"
|
||||
|
||||
|
||||
@@ -60,6 +60,7 @@ fn bench_match_exact(b: &mut Bencher) {
|
||||
true,
|
||||
vec![],
|
||||
false,
|
||||
false,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
@@ -105,6 +106,7 @@ fn bench_match_word(b: &mut Bencher) {
|
||||
true,
|
||||
vec![],
|
||||
false,
|
||||
false,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
@@ -150,6 +152,7 @@ fn bench_match_word_miss(b: &mut Bencher) {
|
||||
true,
|
||||
vec![],
|
||||
false,
|
||||
false,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
@@ -195,6 +198,7 @@ fn bench_eval_message(b: &mut Bencher) {
|
||||
true,
|
||||
vec![],
|
||||
false,
|
||||
false,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
@@ -205,6 +209,7 @@ fn bench_eval_message(b: &mut Bencher) {
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
);
|
||||
|
||||
b.iter(|| eval.run(&rules, Some("bob"), Some("person")));
|
||||
|
||||
@@ -81,7 +81,7 @@ pub const BASE_APPEND_OVERRIDE_RULES: &[PushRule] = &[
|
||||
))]),
|
||||
actions: Cow::Borrowed(&[Action::Notify]),
|
||||
default: true,
|
||||
default_enabled: false,
|
||||
default_enabled: true,
|
||||
},
|
||||
PushRule {
|
||||
rule_id: Cow::Borrowed("global/override/.m.rule.suppress_notices"),
|
||||
|
||||
@@ -105,6 +105,9 @@ pub struct PushRuleEvaluator {
|
||||
/// If MSC3931 (room version feature flags) is enabled. Usually controlled by the same
|
||||
/// flag as MSC1767 (extensible events core).
|
||||
msc3931_enabled: bool,
|
||||
|
||||
// If MSC4210 (remove legacy mentions) is enabled.
|
||||
msc4210_enabled: bool,
|
||||
}
|
||||
|
||||
#[pymethods]
|
||||
@@ -122,6 +125,7 @@ impl PushRuleEvaluator {
|
||||
related_event_match_enabled,
|
||||
room_version_feature_flags,
|
||||
msc3931_enabled,
|
||||
msc4210_enabled,
|
||||
))]
|
||||
pub fn py_new(
|
||||
flattened_keys: BTreeMap<String, JsonValue>,
|
||||
@@ -133,6 +137,7 @@ impl PushRuleEvaluator {
|
||||
related_event_match_enabled: bool,
|
||||
room_version_feature_flags: Vec<String>,
|
||||
msc3931_enabled: bool,
|
||||
msc4210_enabled: bool,
|
||||
) -> Result<Self, Error> {
|
||||
let body = match flattened_keys.get("content.body") {
|
||||
Some(JsonValue::Value(SimpleJsonValue::Str(s))) => s.clone().into_owned(),
|
||||
@@ -150,6 +155,7 @@ impl PushRuleEvaluator {
|
||||
related_event_match_enabled,
|
||||
room_version_feature_flags,
|
||||
msc3931_enabled,
|
||||
msc4210_enabled,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -176,7 +182,8 @@ impl PushRuleEvaluator {
|
||||
|
||||
// For backwards-compatibility the legacy mention rules are disabled
|
||||
// if the event contains the 'm.mentions' property.
|
||||
if self.has_mentions
|
||||
// Additionally, MSC4210 always disables the legacy rules.
|
||||
if (self.has_mentions || self.msc4210_enabled)
|
||||
&& (rule_id == "global/override/.m.rule.contains_display_name"
|
||||
|| rule_id == "global/content/.m.rule.contains_user_name"
|
||||
|| rule_id == "global/override/.m.rule.roomnotif")
|
||||
@@ -526,6 +533,7 @@ fn push_rule_evaluator() {
|
||||
true,
|
||||
vec![],
|
||||
true,
|
||||
false,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
@@ -555,6 +563,7 @@ fn test_requires_room_version_supports_condition() {
|
||||
false,
|
||||
flags,
|
||||
true,
|
||||
false,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
@@ -582,7 +591,7 @@ fn test_requires_room_version_supports_condition() {
|
||||
};
|
||||
let rules = PushRules::new(vec![custom_rule]);
|
||||
result = evaluator.run(
|
||||
&FilteredPushRules::py_new(rules, BTreeMap::new(), true, false, true, false),
|
||||
&FilteredPushRules::py_new(rules, BTreeMap::new(), true, false, true, false, false),
|
||||
None,
|
||||
None,
|
||||
);
|
||||
|
||||
@@ -534,6 +534,7 @@ pub struct FilteredPushRules {
|
||||
msc3381_polls_enabled: bool,
|
||||
msc3664_enabled: bool,
|
||||
msc4028_push_encrypted_events: bool,
|
||||
msc4210_enabled: bool,
|
||||
}
|
||||
|
||||
#[pymethods]
|
||||
@@ -546,6 +547,7 @@ impl FilteredPushRules {
|
||||
msc3381_polls_enabled: bool,
|
||||
msc3664_enabled: bool,
|
||||
msc4028_push_encrypted_events: bool,
|
||||
msc4210_enabled: bool,
|
||||
) -> Self {
|
||||
Self {
|
||||
push_rules,
|
||||
@@ -554,6 +556,7 @@ impl FilteredPushRules {
|
||||
msc3381_polls_enabled,
|
||||
msc3664_enabled,
|
||||
msc4028_push_encrypted_events,
|
||||
msc4210_enabled,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -596,6 +599,14 @@ impl FilteredPushRules {
|
||||
return false;
|
||||
}
|
||||
|
||||
if self.msc4210_enabled
|
||||
&& (rule.rule_id == "global/override/.m.rule.contains_display_name"
|
||||
|| rule.rule_id == "global/content/.m.rule.contains_user_name"
|
||||
|| rule.rule_id == "global/override/.m.rule.roomnotif")
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
true
|
||||
})
|
||||
.map(|r| {
|
||||
|
||||
@@ -32,8 +32,8 @@ DISTS = (
|
||||
"debian:sid", # (EOL not specified yet) (our EOL forced by Python 3.11 is 2027-10-24)
|
||||
"ubuntu:focal", # 20.04 LTS (EOL 2025-04) (our EOL forced by Python 3.8 is 2024-10-14)
|
||||
"ubuntu:jammy", # 22.04 LTS (EOL 2027-04) (our EOL forced by Python 3.10 is 2026-10-04)
|
||||
"ubuntu:lunar", # 23.04 (EOL 2024-01) (our EOL forced by Python 3.11 is 2027-10-24)
|
||||
"ubuntu:mantic", # 23.10 (EOL 2024-07) (our EOL forced by Python 3.11 is 2027-10-24)
|
||||
"ubuntu:noble", # 24.04 LTS (EOL 2029-06)
|
||||
"ubuntu:oracular", # 24.10 (EOL 2025-07)
|
||||
"debian:trixie", # (EOL not specified yet)
|
||||
)
|
||||
|
||||
|
||||
@@ -45,7 +45,6 @@ import traceback
|
||||
import unittest.mock
|
||||
from contextlib import contextmanager
|
||||
from typing import (
|
||||
TYPE_CHECKING,
|
||||
Any,
|
||||
Callable,
|
||||
Dict,
|
||||
@@ -57,30 +56,17 @@ from typing import (
|
||||
)
|
||||
|
||||
from parameterized import parameterized
|
||||
|
||||
from synapse._pydantic_compat import HAS_PYDANTIC_V2
|
||||
|
||||
if TYPE_CHECKING or HAS_PYDANTIC_V2:
|
||||
from pydantic.v1 import (
|
||||
BaseModel as PydanticBaseModel,
|
||||
conbytes,
|
||||
confloat,
|
||||
conint,
|
||||
constr,
|
||||
)
|
||||
from pydantic.v1.typing import get_args
|
||||
else:
|
||||
from pydantic import (
|
||||
BaseModel as PydanticBaseModel,
|
||||
conbytes,
|
||||
confloat,
|
||||
conint,
|
||||
constr,
|
||||
)
|
||||
from pydantic.typing import get_args
|
||||
|
||||
from typing_extensions import ParamSpec
|
||||
|
||||
from synapse._pydantic_compat import (
|
||||
BaseModel as PydanticBaseModel,
|
||||
conbytes,
|
||||
confloat,
|
||||
conint,
|
||||
constr,
|
||||
get_args,
|
||||
)
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
CONSTRAINED_TYPE_FACTORIES_WITH_STRICT_FLAG: List[Callable] = [
|
||||
@@ -183,22 +169,16 @@ def monkeypatch_pydantic() -> Generator[None, None, None]:
|
||||
# Most Synapse code ought to import the patched objects directly from
|
||||
# `pydantic`. But we also patch their containing modules `pydantic.main` and
|
||||
# `pydantic.types` for completeness.
|
||||
patch_basemodel1 = unittest.mock.patch(
|
||||
"pydantic.BaseModel", new=PatchedBaseModel
|
||||
patch_basemodel = unittest.mock.patch(
|
||||
"synapse._pydantic_compat.BaseModel", new=PatchedBaseModel
|
||||
)
|
||||
patch_basemodel2 = unittest.mock.patch(
|
||||
"pydantic.main.BaseModel", new=PatchedBaseModel
|
||||
)
|
||||
patches.enter_context(patch_basemodel1)
|
||||
patches.enter_context(patch_basemodel2)
|
||||
patches.enter_context(patch_basemodel)
|
||||
for factory in CONSTRAINED_TYPE_FACTORIES_WITH_STRICT_FLAG:
|
||||
wrapper: Callable = make_wrapper(factory)
|
||||
patch1 = unittest.mock.patch(f"pydantic.{factory.__name__}", new=wrapper)
|
||||
patch2 = unittest.mock.patch(
|
||||
f"pydantic.types.{factory.__name__}", new=wrapper
|
||||
patch = unittest.mock.patch(
|
||||
f"synapse._pydantic_compat.{factory.__name__}", new=wrapper
|
||||
)
|
||||
patches.enter_context(patch1)
|
||||
patches.enter_context(patch2)
|
||||
patches.enter_context(patch)
|
||||
yield
|
||||
|
||||
|
||||
|
||||
@@ -220,9 +220,11 @@ test_packages=(
|
||||
./tests/msc3874
|
||||
./tests/msc3890
|
||||
./tests/msc3391
|
||||
./tests/msc3757
|
||||
./tests/msc3930
|
||||
./tests/msc3902
|
||||
./tests/msc3967
|
||||
./tests/msc4140
|
||||
)
|
||||
|
||||
# Enable dirty runs, so tests will reuse the same container where possible.
|
||||
|
||||
@@ -360,7 +360,7 @@ def is_cacheable(
|
||||
# For a type alias, check if the underlying real type is cachable.
|
||||
return is_cacheable(mypy.types.get_proper_type(rt), signature, verbose)
|
||||
|
||||
elif isinstance(rt, UninhabitedType) and rt.is_noreturn:
|
||||
elif isinstance(rt, UninhabitedType):
|
||||
# There is no return value, just consider it cachable. This is only used
|
||||
# in tests.
|
||||
return True, None
|
||||
|
||||
@@ -40,7 +40,7 @@ import commonmark
|
||||
import git
|
||||
from click.exceptions import ClickException
|
||||
from git import GitCommandError, Repo
|
||||
from github import Github
|
||||
from github import BadCredentialsException, Github
|
||||
from packaging import version
|
||||
|
||||
|
||||
@@ -323,10 +323,8 @@ def tag(gh_token: Optional[str]) -> None:
|
||||
def _tag(gh_token: Optional[str]) -> None:
|
||||
"""Tags the release and generates a draft GitHub release"""
|
||||
|
||||
if gh_token:
|
||||
# Test that the GH Token is valid before continuing.
|
||||
gh = Github(gh_token)
|
||||
gh.get_user()
|
||||
# Test that the GH Token is valid before continuing.
|
||||
check_valid_gh_token(gh_token)
|
||||
|
||||
# Make sure we're in a git repo.
|
||||
repo = get_repo_and_check_clean_checkout()
|
||||
@@ -469,10 +467,8 @@ def upload(gh_token: Optional[str]) -> None:
|
||||
def _upload(gh_token: Optional[str]) -> None:
|
||||
"""Upload release to pypi."""
|
||||
|
||||
if gh_token:
|
||||
# Test that the GH Token is valid before continuing.
|
||||
gh = Github(gh_token)
|
||||
gh.get_user()
|
||||
# Test that the GH Token is valid before continuing.
|
||||
check_valid_gh_token(gh_token)
|
||||
|
||||
current_version = get_package_version()
|
||||
tag_name = f"v{current_version}"
|
||||
@@ -569,10 +565,8 @@ def wait_for_actions(gh_token: Optional[str]) -> None:
|
||||
|
||||
|
||||
def _wait_for_actions(gh_token: Optional[str]) -> None:
|
||||
if gh_token:
|
||||
# Test that the GH Token is valid before continuing.
|
||||
gh = Github(gh_token)
|
||||
gh.get_user()
|
||||
# Test that the GH Token is valid before continuing.
|
||||
check_valid_gh_token(gh_token)
|
||||
|
||||
# Find out the version and tag name.
|
||||
current_version = get_package_version()
|
||||
@@ -806,6 +800,22 @@ def get_repo_and_check_clean_checkout(
|
||||
return repo
|
||||
|
||||
|
||||
def check_valid_gh_token(gh_token: Optional[str]) -> None:
|
||||
"""Check that a github token is valid, if supplied"""
|
||||
|
||||
if not gh_token:
|
||||
# No github token supplied, so nothing to do.
|
||||
return
|
||||
|
||||
try:
|
||||
gh = Github(gh_token)
|
||||
|
||||
# We need to lookup name to trigger a request.
|
||||
_name = gh.get_user().name
|
||||
except BadCredentialsException as e:
|
||||
raise click.ClickException(f"Github credentials are bad: {e}")
|
||||
|
||||
|
||||
def find_ref(repo: git.Repo, ref_name: str) -> Optional[git.HEAD]:
|
||||
"""Find the branch/ref, looking first locally then in the remote."""
|
||||
if ref_name in repo.references:
|
||||
|
||||
@@ -19,6 +19,8 @@
|
||||
#
|
||||
#
|
||||
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from packaging.version import Version
|
||||
|
||||
try:
|
||||
@@ -30,4 +32,64 @@ except ImportError:
|
||||
|
||||
HAS_PYDANTIC_V2: bool = Version(pydantic_version).major == 2
|
||||
|
||||
__all__ = ("HAS_PYDANTIC_V2",)
|
||||
if TYPE_CHECKING or HAS_PYDANTIC_V2:
|
||||
from pydantic.v1 import (
|
||||
BaseModel,
|
||||
Extra,
|
||||
Field,
|
||||
MissingError,
|
||||
PydanticValueError,
|
||||
StrictBool,
|
||||
StrictInt,
|
||||
StrictStr,
|
||||
ValidationError,
|
||||
conbytes,
|
||||
confloat,
|
||||
conint,
|
||||
constr,
|
||||
parse_obj_as,
|
||||
validator,
|
||||
)
|
||||
from pydantic.v1.error_wrappers import ErrorWrapper
|
||||
from pydantic.v1.typing import get_args
|
||||
else:
|
||||
from pydantic import (
|
||||
BaseModel,
|
||||
Extra,
|
||||
Field,
|
||||
MissingError,
|
||||
PydanticValueError,
|
||||
StrictBool,
|
||||
StrictInt,
|
||||
StrictStr,
|
||||
ValidationError,
|
||||
conbytes,
|
||||
confloat,
|
||||
conint,
|
||||
constr,
|
||||
parse_obj_as,
|
||||
validator,
|
||||
)
|
||||
from pydantic.error_wrappers import ErrorWrapper
|
||||
from pydantic.typing import get_args
|
||||
|
||||
__all__ = (
|
||||
"HAS_PYDANTIC_V2",
|
||||
"BaseModel",
|
||||
"constr",
|
||||
"conbytes",
|
||||
"conint",
|
||||
"confloat",
|
||||
"ErrorWrapper",
|
||||
"Extra",
|
||||
"Field",
|
||||
"get_args",
|
||||
"MissingError",
|
||||
"parse_obj_as",
|
||||
"PydanticValueError",
|
||||
"StrictBool",
|
||||
"StrictInt",
|
||||
"StrictStr",
|
||||
"ValidationError",
|
||||
"validator",
|
||||
)
|
||||
|
||||
@@ -338,7 +338,7 @@ class MSC3861DelegatedAuth(BaseAuth):
|
||||
logger.exception("Failed to introspect token")
|
||||
raise SynapseError(503, "Unable to introspect the access token")
|
||||
|
||||
logger.info(f"Introspection result: {introspection_result!r}")
|
||||
logger.debug("Introspection result: %r", introspection_result)
|
||||
|
||||
# TODO: introspection verification should be more extensive, especially:
|
||||
# - verify the audience
|
||||
|
||||
@@ -107,6 +107,8 @@ class RoomVersion:
|
||||
# support the flag. Unknown flags are ignored by the evaluator, making conditions
|
||||
# fail if used.
|
||||
msc3931_push_features: Tuple[str, ...] # values from PushRuleRoomFlag
|
||||
# MSC3757: Restricting who can overwrite a state event
|
||||
msc3757_enabled: bool
|
||||
|
||||
|
||||
class RoomVersions:
|
||||
@@ -128,6 +130,7 @@ class RoomVersions:
|
||||
knock_restricted_join_rule=False,
|
||||
enforce_int_power_levels=False,
|
||||
msc3931_push_features=(),
|
||||
msc3757_enabled=False,
|
||||
)
|
||||
V2 = RoomVersion(
|
||||
"2",
|
||||
@@ -147,6 +150,7 @@ class RoomVersions:
|
||||
knock_restricted_join_rule=False,
|
||||
enforce_int_power_levels=False,
|
||||
msc3931_push_features=(),
|
||||
msc3757_enabled=False,
|
||||
)
|
||||
V3 = RoomVersion(
|
||||
"3",
|
||||
@@ -166,6 +170,7 @@ class RoomVersions:
|
||||
knock_restricted_join_rule=False,
|
||||
enforce_int_power_levels=False,
|
||||
msc3931_push_features=(),
|
||||
msc3757_enabled=False,
|
||||
)
|
||||
V4 = RoomVersion(
|
||||
"4",
|
||||
@@ -185,6 +190,7 @@ class RoomVersions:
|
||||
knock_restricted_join_rule=False,
|
||||
enforce_int_power_levels=False,
|
||||
msc3931_push_features=(),
|
||||
msc3757_enabled=False,
|
||||
)
|
||||
V5 = RoomVersion(
|
||||
"5",
|
||||
@@ -204,6 +210,7 @@ class RoomVersions:
|
||||
knock_restricted_join_rule=False,
|
||||
enforce_int_power_levels=False,
|
||||
msc3931_push_features=(),
|
||||
msc3757_enabled=False,
|
||||
)
|
||||
V6 = RoomVersion(
|
||||
"6",
|
||||
@@ -223,6 +230,7 @@ class RoomVersions:
|
||||
knock_restricted_join_rule=False,
|
||||
enforce_int_power_levels=False,
|
||||
msc3931_push_features=(),
|
||||
msc3757_enabled=False,
|
||||
)
|
||||
V7 = RoomVersion(
|
||||
"7",
|
||||
@@ -242,6 +250,7 @@ class RoomVersions:
|
||||
knock_restricted_join_rule=False,
|
||||
enforce_int_power_levels=False,
|
||||
msc3931_push_features=(),
|
||||
msc3757_enabled=False,
|
||||
)
|
||||
V8 = RoomVersion(
|
||||
"8",
|
||||
@@ -261,6 +270,7 @@ class RoomVersions:
|
||||
knock_restricted_join_rule=False,
|
||||
enforce_int_power_levels=False,
|
||||
msc3931_push_features=(),
|
||||
msc3757_enabled=False,
|
||||
)
|
||||
V9 = RoomVersion(
|
||||
"9",
|
||||
@@ -280,6 +290,7 @@ class RoomVersions:
|
||||
knock_restricted_join_rule=False,
|
||||
enforce_int_power_levels=False,
|
||||
msc3931_push_features=(),
|
||||
msc3757_enabled=False,
|
||||
)
|
||||
V10 = RoomVersion(
|
||||
"10",
|
||||
@@ -299,6 +310,7 @@ class RoomVersions:
|
||||
knock_restricted_join_rule=True,
|
||||
enforce_int_power_levels=True,
|
||||
msc3931_push_features=(),
|
||||
msc3757_enabled=False,
|
||||
)
|
||||
MSC1767v10 = RoomVersion(
|
||||
# MSC1767 (Extensible Events) based on room version "10"
|
||||
@@ -319,6 +331,28 @@ class RoomVersions:
|
||||
knock_restricted_join_rule=True,
|
||||
enforce_int_power_levels=True,
|
||||
msc3931_push_features=(PushRuleRoomFlag.EXTENSIBLE_EVENTS,),
|
||||
msc3757_enabled=False,
|
||||
)
|
||||
MSC3757v10 = RoomVersion(
|
||||
# MSC3757 (Restricting who can overwrite a state event) based on room version "10"
|
||||
"org.matrix.msc3757.10",
|
||||
RoomDisposition.UNSTABLE,
|
||||
EventFormatVersions.ROOM_V4_PLUS,
|
||||
StateResolutionVersions.V2,
|
||||
enforce_key_validity=True,
|
||||
special_case_aliases_auth=False,
|
||||
strict_canonicaljson=True,
|
||||
limit_notifications_power_levels=True,
|
||||
implicit_room_creator=False,
|
||||
updated_redaction_rules=False,
|
||||
restricted_join_rule=True,
|
||||
restricted_join_rule_fix=True,
|
||||
knock_join_rule=True,
|
||||
msc3389_relation_redactions=False,
|
||||
knock_restricted_join_rule=True,
|
||||
enforce_int_power_levels=True,
|
||||
msc3931_push_features=(),
|
||||
msc3757_enabled=True,
|
||||
)
|
||||
V11 = RoomVersion(
|
||||
"11",
|
||||
@@ -338,6 +372,28 @@ class RoomVersions:
|
||||
knock_restricted_join_rule=True,
|
||||
enforce_int_power_levels=True,
|
||||
msc3931_push_features=(),
|
||||
msc3757_enabled=False,
|
||||
)
|
||||
MSC3757v11 = RoomVersion(
|
||||
# MSC3757 (Restricting who can overwrite a state event) based on room version "11"
|
||||
"org.matrix.msc3757.11",
|
||||
RoomDisposition.UNSTABLE,
|
||||
EventFormatVersions.ROOM_V4_PLUS,
|
||||
StateResolutionVersions.V2,
|
||||
enforce_key_validity=True,
|
||||
special_case_aliases_auth=False,
|
||||
strict_canonicaljson=True,
|
||||
limit_notifications_power_levels=True,
|
||||
implicit_room_creator=True, # Used by MSC3820
|
||||
updated_redaction_rules=True, # Used by MSC3820
|
||||
restricted_join_rule=True,
|
||||
restricted_join_rule_fix=True,
|
||||
knock_join_rule=True,
|
||||
msc3389_relation_redactions=False,
|
||||
knock_restricted_join_rule=True,
|
||||
enforce_int_power_levels=True,
|
||||
msc3931_push_features=(),
|
||||
msc3757_enabled=True,
|
||||
)
|
||||
|
||||
|
||||
@@ -355,6 +411,8 @@ KNOWN_ROOM_VERSIONS: Dict[str, RoomVersion] = {
|
||||
RoomVersions.V9,
|
||||
RoomVersions.V10,
|
||||
RoomVersions.V11,
|
||||
RoomVersions.MSC3757v10,
|
||||
RoomVersions.MSC3757v11,
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
#
|
||||
# Copyright 2020 The Matrix.org Foundation C.I.C.
|
||||
# Copyright 2016 OpenMarket Ltd
|
||||
# Copyright (C) 2023 New Vector, Ltd
|
||||
# Copyright (C) 2023-2024 New Vector, Ltd
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU Affero General Public License as
|
||||
@@ -65,6 +65,7 @@ from synapse.storage.databases.main.appservice import (
|
||||
)
|
||||
from synapse.storage.databases.main.censor_events import CensorEventsStore
|
||||
from synapse.storage.databases.main.client_ips import ClientIpWorkerStore
|
||||
from synapse.storage.databases.main.delayed_events import DelayedEventsStore
|
||||
from synapse.storage.databases.main.deviceinbox import DeviceInboxWorkerStore
|
||||
from synapse.storage.databases.main.devices import DeviceWorkerStore
|
||||
from synapse.storage.databases.main.directory import DirectoryWorkerStore
|
||||
@@ -161,6 +162,7 @@ class GenericWorkerStore(
|
||||
TaskSchedulerWorkerStore,
|
||||
ExperimentalFeaturesStore,
|
||||
SlidingSyncStore,
|
||||
DelayedEventsStore,
|
||||
):
|
||||
# Properties that multiple storage classes define. Tell mypy what the
|
||||
# expected type is.
|
||||
|
||||
@@ -18,17 +18,11 @@
|
||||
# [This file includes modifications made by New Vector Limited]
|
||||
#
|
||||
#
|
||||
from typing import TYPE_CHECKING, Any, Dict, Type, TypeVar
|
||||
from typing import Any, Dict, Type, TypeVar
|
||||
|
||||
import jsonschema
|
||||
|
||||
from synapse._pydantic_compat import HAS_PYDANTIC_V2
|
||||
|
||||
if TYPE_CHECKING or HAS_PYDANTIC_V2:
|
||||
from pydantic.v1 import BaseModel, ValidationError, parse_obj_as
|
||||
else:
|
||||
from pydantic import BaseModel, ValidationError, parse_obj_as
|
||||
|
||||
from synapse._pydantic_compat import BaseModel, ValidationError, parse_obj_as
|
||||
from synapse.config._base import ConfigError
|
||||
from synapse.types import JsonDict, StrSequence
|
||||
|
||||
|
||||
@@ -447,3 +447,6 @@ class ExperimentalConfig(Config):
|
||||
|
||||
# MSC4151: Report room API (Client-Server API)
|
||||
self.msc4151_enabled: bool = experimental.get("msc4151_enabled", False)
|
||||
|
||||
# MSC4210: Remove legacy mentions
|
||||
self.msc4210_enabled: bool = experimental.get("msc4210_enabled", False)
|
||||
|
||||
@@ -38,6 +38,7 @@ class JWTConfig(Config):
|
||||
self.jwt_algorithm = jwt_config["algorithm"]
|
||||
|
||||
self.jwt_subject_claim = jwt_config.get("subject_claim", "sub")
|
||||
self.jwt_display_name_claim = jwt_config.get("display_name_claim")
|
||||
|
||||
# The issuer and audiences are optional, if provided, it is asserted
|
||||
# that the claims exist on the JWT.
|
||||
@@ -49,5 +50,6 @@ class JWTConfig(Config):
|
||||
self.jwt_secret = None
|
||||
self.jwt_algorithm = None
|
||||
self.jwt_subject_claim = None
|
||||
self.jwt_display_name_claim = None
|
||||
self.jwt_issuer = None
|
||||
self.jwt_audiences = None
|
||||
|
||||
@@ -21,10 +21,15 @@
|
||||
|
||||
from typing import Any
|
||||
|
||||
from synapse.config._base import Config
|
||||
from synapse.config._base import Config, ConfigError, read_file
|
||||
from synapse.types import JsonDict
|
||||
from synapse.util.check_dependencies import check_requirements
|
||||
|
||||
CONFLICTING_PASSWORD_OPTS_ERROR = """\
|
||||
You have configured both `redis.password` and `redis.password_path`.
|
||||
These are mutually incompatible.
|
||||
"""
|
||||
|
||||
|
||||
class RedisConfig(Config):
|
||||
section = "redis"
|
||||
@@ -43,6 +48,17 @@ class RedisConfig(Config):
|
||||
self.redis_path = redis_config.get("path", None)
|
||||
self.redis_dbid = redis_config.get("dbid", None)
|
||||
self.redis_password = redis_config.get("password")
|
||||
redis_password_path = redis_config.get("password_path")
|
||||
if redis_password_path:
|
||||
if self.redis_password:
|
||||
raise ConfigError(CONFLICTING_PASSWORD_OPTS_ERROR)
|
||||
self.redis_password = read_file(
|
||||
redis_password_path,
|
||||
(
|
||||
"redis",
|
||||
"password_path",
|
||||
),
|
||||
).strip()
|
||||
|
||||
self.redis_use_tls = redis_config.get("use_tls", False)
|
||||
self.redis_certificate = redis_config.get("certificate_file", None)
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
# This file is licensed under the Affero General Public License (AGPL) version 3.
|
||||
#
|
||||
# Copyright 2014-2021 The Matrix.org Foundation C.I.C.
|
||||
# Copyright (C) 2023 New Vector, Ltd
|
||||
# Copyright (C) 2023-2024 New Vector, Ltd
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU Affero General Public License as
|
||||
@@ -780,6 +780,17 @@ class ServerConfig(Config):
|
||||
else:
|
||||
self.delete_stale_devices_after = None
|
||||
|
||||
# The maximum allowed delay duration for delayed events (MSC4140).
|
||||
max_event_delay_duration = config.get("max_event_delay_duration")
|
||||
if max_event_delay_duration is not None:
|
||||
self.max_event_delay_ms: Optional[int] = self.parse_duration(
|
||||
max_event_delay_duration
|
||||
)
|
||||
if self.max_event_delay_ms <= 0:
|
||||
raise ConfigError("max_event_delay_duration must be a positive value")
|
||||
else:
|
||||
self.max_event_delay_ms = None
|
||||
|
||||
def has_tls_listener(self) -> bool:
|
||||
return any(listener.is_tls() for listener in self.listeners)
|
||||
|
||||
|
||||
@@ -22,17 +22,17 @@
|
||||
|
||||
import argparse
|
||||
import logging
|
||||
from typing import TYPE_CHECKING, Any, Dict, List, Optional, Union
|
||||
from typing import Any, Dict, List, Optional, Union
|
||||
|
||||
import attr
|
||||
|
||||
from synapse._pydantic_compat import HAS_PYDANTIC_V2
|
||||
|
||||
if TYPE_CHECKING or HAS_PYDANTIC_V2:
|
||||
from pydantic.v1 import BaseModel, Extra, StrictBool, StrictInt, StrictStr
|
||||
else:
|
||||
from pydantic import BaseModel, Extra, StrictBool, StrictInt, StrictStr
|
||||
|
||||
from synapse._pydantic_compat import (
|
||||
BaseModel,
|
||||
Extra,
|
||||
StrictBool,
|
||||
StrictInt,
|
||||
StrictStr,
|
||||
)
|
||||
from synapse.config._base import (
|
||||
Config,
|
||||
ConfigError,
|
||||
|
||||
@@ -388,6 +388,7 @@ LENIENT_EVENT_BYTE_LIMITS_ROOM_VERSIONS = {
|
||||
RoomVersions.V9,
|
||||
RoomVersions.V10,
|
||||
RoomVersions.MSC1767v10,
|
||||
RoomVersions.MSC3757v10,
|
||||
}
|
||||
|
||||
|
||||
@@ -790,9 +791,10 @@ def get_send_level(
|
||||
|
||||
|
||||
def _can_send_event(event: "EventBase", auth_events: StateMap["EventBase"]) -> bool:
|
||||
state_key = event.get_state_key()
|
||||
power_levels_event = get_power_level_event(auth_events)
|
||||
|
||||
send_level = get_send_level(event.type, event.get("state_key"), power_levels_event)
|
||||
send_level = get_send_level(event.type, state_key, power_levels_event)
|
||||
user_level = get_user_power_level(event.user_id, auth_events)
|
||||
|
||||
if user_level < send_level:
|
||||
@@ -803,11 +805,34 @@ def _can_send_event(event: "EventBase", auth_events: StateMap["EventBase"]) -> b
|
||||
errcode=Codes.INSUFFICIENT_POWER,
|
||||
)
|
||||
|
||||
# Check state_key
|
||||
if hasattr(event, "state_key"):
|
||||
if event.state_key.startswith("@"):
|
||||
if event.state_key != event.user_id:
|
||||
raise AuthError(403, "You are not allowed to set others state")
|
||||
if (
|
||||
state_key is not None
|
||||
and state_key.startswith("@")
|
||||
and state_key != event.user_id
|
||||
):
|
||||
if event.room_version.msc3757_enabled:
|
||||
try:
|
||||
colon_idx = state_key.index(":", 1)
|
||||
suffix_idx = state_key.find("_", colon_idx + 1)
|
||||
state_key_user_id = (
|
||||
state_key[:suffix_idx] if suffix_idx != -1 else state_key
|
||||
)
|
||||
if not UserID.is_valid(state_key_user_id):
|
||||
raise ValueError
|
||||
except ValueError:
|
||||
raise SynapseError(
|
||||
400,
|
||||
"State key neither equals a valid user ID, nor starts with one plus an underscore",
|
||||
errcode=Codes.BAD_JSON,
|
||||
)
|
||||
if (
|
||||
# sender is owner of the state key
|
||||
state_key_user_id == event.user_id
|
||||
# sender has higher PL than the owner of the state key
|
||||
or user_level > get_user_power_level(state_key_user_id, auth_events)
|
||||
):
|
||||
return True
|
||||
raise AuthError(403, "You are not allowed to set others state")
|
||||
|
||||
return True
|
||||
|
||||
|
||||
@@ -19,17 +19,11 @@
|
||||
#
|
||||
#
|
||||
import collections.abc
|
||||
from typing import TYPE_CHECKING, List, Type, Union, cast
|
||||
from typing import List, Type, Union, cast
|
||||
|
||||
import jsonschema
|
||||
|
||||
from synapse._pydantic_compat import HAS_PYDANTIC_V2
|
||||
|
||||
if TYPE_CHECKING or HAS_PYDANTIC_V2:
|
||||
from pydantic.v1 import Field, StrictBool, StrictStr
|
||||
else:
|
||||
from pydantic import Field, StrictBool, StrictStr
|
||||
|
||||
from synapse._pydantic_compat import Field, StrictBool, StrictStr
|
||||
from synapse.api.constants import (
|
||||
MAX_ALIAS_LENGTH,
|
||||
EventContentFields,
|
||||
|
||||
@@ -33,7 +33,7 @@ from synapse.replication.http.account_data import (
|
||||
ReplicationRemoveUserAccountDataRestServlet,
|
||||
)
|
||||
from synapse.streams import EventSource
|
||||
from synapse.types import JsonDict, StrCollection, StreamKeyType, UserID
|
||||
from synapse.types import JsonDict, JsonMapping, StrCollection, StreamKeyType, UserID
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from synapse.server import HomeServer
|
||||
@@ -253,7 +253,7 @@ class AccountDataHandler:
|
||||
return response["max_stream_id"]
|
||||
|
||||
async def add_tag_to_room(
|
||||
self, user_id: str, room_id: str, tag: str, content: JsonDict
|
||||
self, user_id: str, room_id: str, tag: str, content: JsonMapping
|
||||
) -> int:
|
||||
"""Add a tag to a room for a user.
|
||||
|
||||
|
||||
@@ -21,13 +21,34 @@
|
||||
|
||||
import abc
|
||||
import logging
|
||||
from typing import TYPE_CHECKING, Any, Dict, List, Mapping, Optional, Sequence, Set
|
||||
from typing import (
|
||||
TYPE_CHECKING,
|
||||
Any,
|
||||
Dict,
|
||||
List,
|
||||
Mapping,
|
||||
Optional,
|
||||
Sequence,
|
||||
Set,
|
||||
Tuple,
|
||||
)
|
||||
|
||||
import attr
|
||||
|
||||
from synapse.api.constants import Direction, Membership
|
||||
from synapse.api.constants import Direction, EventTypes, Membership
|
||||
from synapse.api.errors import SynapseError
|
||||
from synapse.events import EventBase
|
||||
from synapse.types import JsonMapping, RoomStreamToken, StateMap, UserID, UserInfo
|
||||
from synapse.types import (
|
||||
JsonMapping,
|
||||
Requester,
|
||||
RoomStreamToken,
|
||||
ScheduledTask,
|
||||
StateMap,
|
||||
TaskStatus,
|
||||
UserID,
|
||||
UserInfo,
|
||||
create_requester,
|
||||
)
|
||||
from synapse.visibility import filter_events_for_client
|
||||
|
||||
if TYPE_CHECKING:
|
||||
@@ -35,6 +56,8 @@ if TYPE_CHECKING:
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
REDACT_ALL_EVENTS_ACTION_NAME = "redact_all_events"
|
||||
|
||||
|
||||
class AdminHandler:
|
||||
def __init__(self, hs: "HomeServer"):
|
||||
@@ -43,6 +66,20 @@ class AdminHandler:
|
||||
self._storage_controllers = hs.get_storage_controllers()
|
||||
self._state_storage_controller = self._storage_controllers.state
|
||||
self._msc3866_enabled = hs.config.experimental.msc3866.enabled
|
||||
self.event_creation_handler = hs.get_event_creation_handler()
|
||||
self._task_scheduler = hs.get_task_scheduler()
|
||||
|
||||
self._task_scheduler.register_action(
|
||||
self._redact_all_events, REDACT_ALL_EVENTS_ACTION_NAME
|
||||
)
|
||||
|
||||
async def get_redact_task(self, redact_id: str) -> Optional[ScheduledTask]:
|
||||
"""Get the current status of an active redaction process
|
||||
|
||||
Args:
|
||||
redact_id: redact_id returned by start_redact_events.
|
||||
"""
|
||||
return await self._task_scheduler.get_task(redact_id)
|
||||
|
||||
async def get_whois(self, user: UserID) -> JsonMapping:
|
||||
connections = []
|
||||
@@ -313,6 +350,153 @@ class AdminHandler:
|
||||
|
||||
return writer.finished()
|
||||
|
||||
async def start_redact_events(
|
||||
self,
|
||||
user_id: str,
|
||||
rooms: list,
|
||||
requester: JsonMapping,
|
||||
reason: Optional[str],
|
||||
limit: Optional[int],
|
||||
) -> str:
|
||||
"""
|
||||
Start a task redacting the events of the given user in the given rooms
|
||||
|
||||
Args:
|
||||
user_id: the user ID of the user whose events should be redacted
|
||||
rooms: the rooms in which to redact the user's events
|
||||
requester: the user requesting the events
|
||||
reason: reason for requesting the redaction, ie spam, etc
|
||||
limit: limit on the number of events in each room to redact
|
||||
|
||||
Returns:
|
||||
a unique ID which can be used to query the status of the task
|
||||
"""
|
||||
active_tasks = await self._task_scheduler.get_tasks(
|
||||
actions=[REDACT_ALL_EVENTS_ACTION_NAME],
|
||||
resource_id=user_id,
|
||||
statuses=[TaskStatus.ACTIVE],
|
||||
)
|
||||
|
||||
if len(active_tasks) > 0:
|
||||
raise SynapseError(
|
||||
400, "Redact already in progress for user %s" % (user_id,)
|
||||
)
|
||||
|
||||
if not limit:
|
||||
limit = 1000
|
||||
|
||||
redact_id = await self._task_scheduler.schedule_task(
|
||||
REDACT_ALL_EVENTS_ACTION_NAME,
|
||||
resource_id=user_id,
|
||||
params={
|
||||
"rooms": rooms,
|
||||
"requester": requester,
|
||||
"user_id": user_id,
|
||||
"reason": reason,
|
||||
"limit": limit,
|
||||
},
|
||||
)
|
||||
|
||||
logger.info(
|
||||
"starting redact events with redact_id %s",
|
||||
redact_id,
|
||||
)
|
||||
|
||||
return redact_id
|
||||
|
||||
async def _redact_all_events(
|
||||
self, task: ScheduledTask
|
||||
) -> Tuple[TaskStatus, Optional[Mapping[str, Any]], Optional[str]]:
|
||||
"""
|
||||
Task to redact all of a users events in the given rooms, tracking which, if any, events
|
||||
whose redaction failed
|
||||
"""
|
||||
|
||||
assert task.params is not None
|
||||
rooms = task.params.get("rooms")
|
||||
assert rooms is not None
|
||||
|
||||
r = task.params.get("requester")
|
||||
assert r is not None
|
||||
admin = Requester.deserialize(self._store, r)
|
||||
|
||||
user_id = task.params.get("user_id")
|
||||
assert user_id is not None
|
||||
|
||||
requester = create_requester(
|
||||
user_id, authenticated_entity=admin.user.to_string()
|
||||
)
|
||||
|
||||
reason = task.params.get("reason")
|
||||
limit = task.params.get("limit")
|
||||
assert limit is not None
|
||||
|
||||
result: Mapping[str, Any] = (
|
||||
task.result if task.result else {"failed_redactions": {}}
|
||||
)
|
||||
for room in rooms:
|
||||
room_version = await self._store.get_room_version(room)
|
||||
event_ids = await self._store.get_events_sent_by_user_in_room(
|
||||
user_id,
|
||||
room,
|
||||
limit,
|
||||
["m.room.member", "m.room.message"],
|
||||
)
|
||||
if not event_ids:
|
||||
# nothing to redact in this room
|
||||
continue
|
||||
|
||||
events = await self._store.get_events_as_list(event_ids)
|
||||
for event in events:
|
||||
# we care about join events but not other membership events
|
||||
if event.type == "m.room.member":
|
||||
content = event.content
|
||||
if content:
|
||||
if content.get("membership") == Membership.JOIN:
|
||||
pass
|
||||
else:
|
||||
continue
|
||||
relations = await self._store.get_relations_for_event(
|
||||
room, event.event_id, event, event_type=EventTypes.Redaction
|
||||
)
|
||||
|
||||
# if we've already successfully redacted this event then skip processing it
|
||||
if relations[0]:
|
||||
continue
|
||||
|
||||
event_dict = {
|
||||
"type": EventTypes.Redaction,
|
||||
"content": {"reason": reason} if reason else {},
|
||||
"room_id": room,
|
||||
"sender": user_id,
|
||||
}
|
||||
if room_version.updated_redaction_rules:
|
||||
event_dict["content"]["redacts"] = event.event_id
|
||||
else:
|
||||
event_dict["redacts"] = event.event_id
|
||||
|
||||
try:
|
||||
# set the prev event to the offending message to allow for redactions
|
||||
# to be processed in the case where the user has been kicked/banned before
|
||||
# redactions are requested
|
||||
(
|
||||
redaction,
|
||||
_,
|
||||
) = await self.event_creation_handler.create_and_send_nonmember_event(
|
||||
requester,
|
||||
event_dict,
|
||||
prev_event_ids=[event.event_id],
|
||||
ratelimit=False,
|
||||
)
|
||||
except Exception as ex:
|
||||
logger.info(
|
||||
f"Redaction of event {event.event_id} failed due to: {ex}"
|
||||
)
|
||||
result["failed_redactions"][event.event_id] = str(ex)
|
||||
await self._task_scheduler.update_task(task.id, result=result)
|
||||
|
||||
return TaskStatus.COMPLETE, result, None
|
||||
|
||||
|
||||
class ExfiltrationWriter(metaclass=abc.ABCMeta):
|
||||
"""Interface used to specify how to write exported data."""
|
||||
|
||||
498
synapse/handlers/delayed_events.py
Normal file
498
synapse/handlers/delayed_events.py
Normal file
@@ -0,0 +1,498 @@
|
||||
#
|
||||
# This file is licensed under the Affero General Public License (AGPL) version 3.
|
||||
#
|
||||
# Copyright (C) 2024 New Vector, Ltd
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU Affero General Public License as
|
||||
# published by the Free Software Foundation, either version 3 of the
|
||||
# License, or (at your option) any later version.
|
||||
#
|
||||
# See the GNU Affero General Public License for more details:
|
||||
# <https://www.gnu.org/licenses/agpl-3.0.html>.
|
||||
#
|
||||
|
||||
import logging
|
||||
from typing import TYPE_CHECKING, List, Optional, Set, Tuple
|
||||
|
||||
from twisted.internet.interfaces import IDelayedCall
|
||||
|
||||
from synapse.api.constants import EventTypes
|
||||
from synapse.api.errors import ShadowBanError
|
||||
from synapse.config.workers import MAIN_PROCESS_INSTANCE_NAME
|
||||
from synapse.logging.opentracing import set_tag
|
||||
from synapse.metrics import event_processing_positions
|
||||
from synapse.metrics.background_process_metrics import run_as_background_process
|
||||
from synapse.replication.http.delayed_events import (
|
||||
ReplicationAddedDelayedEventRestServlet,
|
||||
)
|
||||
from synapse.storage.databases.main.delayed_events import (
|
||||
DelayedEventDetails,
|
||||
DelayID,
|
||||
EventType,
|
||||
StateKey,
|
||||
Timestamp,
|
||||
UserLocalpart,
|
||||
)
|
||||
from synapse.storage.databases.main.state_deltas import StateDelta
|
||||
from synapse.types import (
|
||||
JsonDict,
|
||||
Requester,
|
||||
RoomID,
|
||||
UserID,
|
||||
create_requester,
|
||||
)
|
||||
from synapse.util.events import generate_fake_event_id
|
||||
from synapse.util.metrics import Measure
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from synapse.server import HomeServer
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class DelayedEventsHandler:
|
||||
def __init__(self, hs: "HomeServer"):
|
||||
self._store = hs.get_datastores().main
|
||||
self._storage_controllers = hs.get_storage_controllers()
|
||||
self._config = hs.config
|
||||
self._clock = hs.get_clock()
|
||||
self._request_ratelimiter = hs.get_request_ratelimiter()
|
||||
self._event_creation_handler = hs.get_event_creation_handler()
|
||||
self._room_member_handler = hs.get_room_member_handler()
|
||||
|
||||
self._next_delayed_event_call: Optional[IDelayedCall] = None
|
||||
|
||||
# The current position in the current_state_delta stream
|
||||
self._event_pos: Optional[int] = None
|
||||
|
||||
# Guard to ensure we only process event deltas one at a time
|
||||
self._event_processing = False
|
||||
|
||||
if hs.config.worker.worker_app is None:
|
||||
self._repl_client = None
|
||||
|
||||
async def _schedule_db_events() -> None:
|
||||
# We kick this off to pick up outstanding work from before the last restart.
|
||||
# Block until we're up to date.
|
||||
await self._unsafe_process_new_event()
|
||||
hs.get_notifier().add_replication_callback(self.notify_new_event)
|
||||
# Kick off again (without blocking) to catch any missed notifications
|
||||
# that may have fired before the callback was added.
|
||||
self._clock.call_later(0, self.notify_new_event)
|
||||
|
||||
# Delayed events that are already marked as processed on startup might not have been
|
||||
# sent properly on the last run of the server, so unmark them to send them again.
|
||||
# Caveat: this will double-send delayed events that successfully persisted, but failed
|
||||
# to be removed from the DB table of delayed events.
|
||||
# TODO: To avoid double-sending, scan the timeline to find which of these events were
|
||||
# already sent. To do so, must store delay_ids in sent events to retrieve them later.
|
||||
await self._store.unprocess_delayed_events()
|
||||
|
||||
events, next_send_ts = await self._store.process_timeout_delayed_events(
|
||||
self._get_current_ts()
|
||||
)
|
||||
|
||||
if next_send_ts:
|
||||
self._schedule_next_at(next_send_ts)
|
||||
|
||||
# Can send the events in background after having awaited on marking them as processed
|
||||
run_as_background_process(
|
||||
"_send_events",
|
||||
self._send_events,
|
||||
events,
|
||||
)
|
||||
|
||||
self._initialized_from_db = run_as_background_process(
|
||||
"_schedule_db_events", _schedule_db_events
|
||||
)
|
||||
else:
|
||||
self._repl_client = ReplicationAddedDelayedEventRestServlet.make_client(hs)
|
||||
|
||||
@property
|
||||
def _is_master(self) -> bool:
|
||||
return self._repl_client is None
|
||||
|
||||
def notify_new_event(self) -> None:
|
||||
"""
|
||||
Called when there may be more state event deltas to process,
|
||||
which should cancel pending delayed events for the same state.
|
||||
"""
|
||||
if self._event_processing:
|
||||
return
|
||||
|
||||
self._event_processing = True
|
||||
|
||||
async def process() -> None:
|
||||
try:
|
||||
await self._unsafe_process_new_event()
|
||||
finally:
|
||||
self._event_processing = False
|
||||
|
||||
run_as_background_process("delayed_events.notify_new_event", process)
|
||||
|
||||
async def _unsafe_process_new_event(self) -> None:
|
||||
# If self._event_pos is None then means we haven't fetched it from the DB yet
|
||||
if self._event_pos is None:
|
||||
self._event_pos = await self._store.get_delayed_events_stream_pos()
|
||||
room_max_stream_ordering = self._store.get_room_max_stream_ordering()
|
||||
if self._event_pos > room_max_stream_ordering:
|
||||
# apparently, we've processed more events than exist in the database!
|
||||
# this can happen if events are removed with history purge or similar.
|
||||
logger.warning(
|
||||
"Event stream ordering appears to have gone backwards (%i -> %i): "
|
||||
"rewinding delayed events processor",
|
||||
self._event_pos,
|
||||
room_max_stream_ordering,
|
||||
)
|
||||
self._event_pos = room_max_stream_ordering
|
||||
|
||||
# Loop round handling deltas until we're up to date
|
||||
while True:
|
||||
with Measure(self._clock, "delayed_events_delta"):
|
||||
room_max_stream_ordering = self._store.get_room_max_stream_ordering()
|
||||
if self._event_pos == room_max_stream_ordering:
|
||||
return
|
||||
|
||||
logger.debug(
|
||||
"Processing delayed events %s->%s",
|
||||
self._event_pos,
|
||||
room_max_stream_ordering,
|
||||
)
|
||||
(
|
||||
max_pos,
|
||||
deltas,
|
||||
) = await self._storage_controllers.state.get_current_state_deltas(
|
||||
self._event_pos, room_max_stream_ordering
|
||||
)
|
||||
|
||||
logger.debug(
|
||||
"Handling %d state deltas for delayed events processing",
|
||||
len(deltas),
|
||||
)
|
||||
await self._handle_state_deltas(deltas)
|
||||
|
||||
self._event_pos = max_pos
|
||||
|
||||
# Expose current event processing position to prometheus
|
||||
event_processing_positions.labels("delayed_events").set(max_pos)
|
||||
|
||||
await self._store.update_delayed_events_stream_pos(max_pos)
|
||||
|
||||
async def _handle_state_deltas(self, deltas: List[StateDelta]) -> None:
|
||||
"""
|
||||
Process current state deltas to cancel pending delayed events
|
||||
that target the same state.
|
||||
"""
|
||||
for delta in deltas:
|
||||
logger.debug(
|
||||
"Handling: %r %r, %s", delta.event_type, delta.state_key, delta.event_id
|
||||
)
|
||||
|
||||
next_send_ts = await self._store.cancel_delayed_state_events(
|
||||
room_id=delta.room_id,
|
||||
event_type=delta.event_type,
|
||||
state_key=delta.state_key,
|
||||
)
|
||||
|
||||
if self._next_send_ts_changed(next_send_ts):
|
||||
self._schedule_next_at_or_none(next_send_ts)
|
||||
|
||||
async def add(
|
||||
self,
|
||||
requester: Requester,
|
||||
*,
|
||||
room_id: str,
|
||||
event_type: str,
|
||||
state_key: Optional[str],
|
||||
origin_server_ts: Optional[int],
|
||||
content: JsonDict,
|
||||
delay: int,
|
||||
) -> str:
|
||||
"""
|
||||
Creates a new delayed event and schedules its delivery.
|
||||
|
||||
Args:
|
||||
requester: The requester of the delayed event, who will be its owner.
|
||||
room_id: The ID of the room where the event should be sent to.
|
||||
event_type: The type of event to be sent.
|
||||
state_key: The state key of the event to be sent, or None if it is not a state event.
|
||||
origin_server_ts: The custom timestamp to send the event with.
|
||||
If None, the timestamp will be the actual time when the event is sent.
|
||||
content: The content of the event to be sent.
|
||||
delay: How long (in milliseconds) to wait before automatically sending the event.
|
||||
|
||||
Returns: The ID of the added delayed event.
|
||||
|
||||
Raises:
|
||||
SynapseError: if the delayed event fails validation checks.
|
||||
"""
|
||||
await self._request_ratelimiter.ratelimit(requester)
|
||||
|
||||
self._event_creation_handler.validator.validate_builder(
|
||||
self._event_creation_handler.event_builder_factory.for_room_version(
|
||||
await self._store.get_room_version(room_id),
|
||||
{
|
||||
"type": event_type,
|
||||
"content": content,
|
||||
"room_id": room_id,
|
||||
"sender": str(requester.user),
|
||||
**({"state_key": state_key} if state_key is not None else {}),
|
||||
},
|
||||
)
|
||||
)
|
||||
|
||||
creation_ts = self._get_current_ts()
|
||||
|
||||
delay_id, next_send_ts = await self._store.add_delayed_event(
|
||||
user_localpart=requester.user.localpart,
|
||||
device_id=requester.device_id,
|
||||
creation_ts=creation_ts,
|
||||
room_id=room_id,
|
||||
event_type=event_type,
|
||||
state_key=state_key,
|
||||
origin_server_ts=origin_server_ts,
|
||||
content=content,
|
||||
delay=delay,
|
||||
)
|
||||
|
||||
if self._repl_client is not None:
|
||||
# NOTE: If this throws, the delayed event will remain in the DB and
|
||||
# will be picked up once the main worker gets another delayed event.
|
||||
await self._repl_client(
|
||||
instance_name=MAIN_PROCESS_INSTANCE_NAME,
|
||||
next_send_ts=next_send_ts,
|
||||
)
|
||||
elif self._next_send_ts_changed(next_send_ts):
|
||||
self._schedule_next_at(next_send_ts)
|
||||
|
||||
return delay_id
|
||||
|
||||
def on_added(self, next_send_ts: int) -> None:
|
||||
next_send_ts = Timestamp(next_send_ts)
|
||||
if self._next_send_ts_changed(next_send_ts):
|
||||
self._schedule_next_at(next_send_ts)
|
||||
|
||||
async def cancel(self, requester: Requester, delay_id: str) -> None:
|
||||
"""
|
||||
Cancels the scheduled delivery of the matching delayed event.
|
||||
|
||||
Args:
|
||||
requester: The owner of the delayed event to act on.
|
||||
delay_id: The ID of the delayed event to act on.
|
||||
|
||||
Raises:
|
||||
NotFoundError: if no matching delayed event could be found.
|
||||
"""
|
||||
assert self._is_master
|
||||
await self._request_ratelimiter.ratelimit(requester)
|
||||
await self._initialized_from_db
|
||||
|
||||
next_send_ts = await self._store.cancel_delayed_event(
|
||||
delay_id=delay_id,
|
||||
user_localpart=requester.user.localpart,
|
||||
)
|
||||
|
||||
if self._next_send_ts_changed(next_send_ts):
|
||||
self._schedule_next_at_or_none(next_send_ts)
|
||||
|
||||
async def restart(self, requester: Requester, delay_id: str) -> None:
|
||||
"""
|
||||
Restarts the scheduled delivery of the matching delayed event.
|
||||
|
||||
Args:
|
||||
requester: The owner of the delayed event to act on.
|
||||
delay_id: The ID of the delayed event to act on.
|
||||
|
||||
Raises:
|
||||
NotFoundError: if no matching delayed event could be found.
|
||||
"""
|
||||
assert self._is_master
|
||||
await self._request_ratelimiter.ratelimit(requester)
|
||||
await self._initialized_from_db
|
||||
|
||||
next_send_ts = await self._store.restart_delayed_event(
|
||||
delay_id=delay_id,
|
||||
user_localpart=requester.user.localpart,
|
||||
current_ts=self._get_current_ts(),
|
||||
)
|
||||
|
||||
if self._next_send_ts_changed(next_send_ts):
|
||||
self._schedule_next_at(next_send_ts)
|
||||
|
||||
async def send(self, requester: Requester, delay_id: str) -> None:
|
||||
"""
|
||||
Immediately sends the matching delayed event, instead of waiting for its scheduled delivery.
|
||||
|
||||
Args:
|
||||
requester: The owner of the delayed event to act on.
|
||||
delay_id: The ID of the delayed event to act on.
|
||||
|
||||
Raises:
|
||||
NotFoundError: if no matching delayed event could be found.
|
||||
"""
|
||||
assert self._is_master
|
||||
await self._request_ratelimiter.ratelimit(requester)
|
||||
await self._initialized_from_db
|
||||
|
||||
event, next_send_ts = await self._store.process_target_delayed_event(
|
||||
delay_id=delay_id,
|
||||
user_localpart=requester.user.localpart,
|
||||
)
|
||||
|
||||
if self._next_send_ts_changed(next_send_ts):
|
||||
self._schedule_next_at_or_none(next_send_ts)
|
||||
|
||||
await self._send_event(
|
||||
DelayedEventDetails(
|
||||
delay_id=DelayID(delay_id),
|
||||
user_localpart=UserLocalpart(requester.user.localpart),
|
||||
room_id=event.room_id,
|
||||
type=event.type,
|
||||
state_key=event.state_key,
|
||||
origin_server_ts=event.origin_server_ts,
|
||||
content=event.content,
|
||||
device_id=event.device_id,
|
||||
)
|
||||
)
|
||||
|
||||
async def _send_on_timeout(self) -> None:
|
||||
self._next_delayed_event_call = None
|
||||
|
||||
events, next_send_ts = await self._store.process_timeout_delayed_events(
|
||||
self._get_current_ts()
|
||||
)
|
||||
|
||||
if next_send_ts:
|
||||
self._schedule_next_at(next_send_ts)
|
||||
|
||||
await self._send_events(events)
|
||||
|
||||
async def _send_events(self, events: List[DelayedEventDetails]) -> None:
|
||||
sent_state: Set[Tuple[RoomID, EventType, StateKey]] = set()
|
||||
for event in events:
|
||||
if event.state_key is not None:
|
||||
state_info = (event.room_id, event.type, event.state_key)
|
||||
if state_info in sent_state:
|
||||
continue
|
||||
else:
|
||||
state_info = None
|
||||
try:
|
||||
# TODO: send in background if message event or non-conflicting state event
|
||||
await self._send_event(event)
|
||||
if state_info is not None:
|
||||
sent_state.add(state_info)
|
||||
except Exception:
|
||||
logger.exception("Failed to send delayed event")
|
||||
|
||||
for room_id, event_type, state_key in sent_state:
|
||||
await self._store.delete_processed_delayed_state_events(
|
||||
room_id=str(room_id),
|
||||
event_type=event_type,
|
||||
state_key=state_key,
|
||||
)
|
||||
|
||||
def _schedule_next_at_or_none(self, next_send_ts: Optional[Timestamp]) -> None:
|
||||
if next_send_ts is not None:
|
||||
self._schedule_next_at(next_send_ts)
|
||||
elif self._next_delayed_event_call is not None:
|
||||
self._next_delayed_event_call.cancel()
|
||||
self._next_delayed_event_call = None
|
||||
|
||||
def _schedule_next_at(self, next_send_ts: Timestamp) -> None:
|
||||
delay = next_send_ts - self._get_current_ts()
|
||||
delay_sec = delay / 1000 if delay > 0 else 0
|
||||
|
||||
if self._next_delayed_event_call is None:
|
||||
self._next_delayed_event_call = self._clock.call_later(
|
||||
delay_sec,
|
||||
run_as_background_process,
|
||||
"_send_on_timeout",
|
||||
self._send_on_timeout,
|
||||
)
|
||||
else:
|
||||
self._next_delayed_event_call.reset(delay_sec)
|
||||
|
||||
async def get_all_for_user(self, requester: Requester) -> List[JsonDict]:
|
||||
"""Return all pending delayed events requested by the given user."""
|
||||
await self._request_ratelimiter.ratelimit(requester)
|
||||
return await self._store.get_all_delayed_events_for_user(
|
||||
requester.user.localpart
|
||||
)
|
||||
|
||||
async def _send_event(
|
||||
self,
|
||||
event: DelayedEventDetails,
|
||||
txn_id: Optional[str] = None,
|
||||
) -> None:
|
||||
user_id = UserID(event.user_localpart, self._config.server.server_name)
|
||||
user_id_str = user_id.to_string()
|
||||
# Create a new requester from what data is currently available
|
||||
requester = create_requester(
|
||||
user_id,
|
||||
is_guest=await self._store.is_guest(user_id_str),
|
||||
device_id=event.device_id,
|
||||
)
|
||||
|
||||
try:
|
||||
if event.state_key is not None and event.type == EventTypes.Member:
|
||||
membership = event.content.get("membership")
|
||||
assert membership is not None
|
||||
event_id, _ = await self._room_member_handler.update_membership(
|
||||
requester,
|
||||
target=UserID.from_string(event.state_key),
|
||||
room_id=event.room_id.to_string(),
|
||||
action=membership,
|
||||
content=event.content,
|
||||
origin_server_ts=event.origin_server_ts,
|
||||
)
|
||||
else:
|
||||
event_dict: JsonDict = {
|
||||
"type": event.type,
|
||||
"content": event.content,
|
||||
"room_id": event.room_id.to_string(),
|
||||
"sender": user_id_str,
|
||||
}
|
||||
|
||||
if event.origin_server_ts is not None:
|
||||
event_dict["origin_server_ts"] = event.origin_server_ts
|
||||
|
||||
if event.state_key is not None:
|
||||
event_dict["state_key"] = event.state_key
|
||||
|
||||
(
|
||||
sent_event,
|
||||
_,
|
||||
) = await self._event_creation_handler.create_and_send_nonmember_event(
|
||||
requester,
|
||||
event_dict,
|
||||
txn_id=txn_id,
|
||||
)
|
||||
event_id = sent_event.event_id
|
||||
except ShadowBanError:
|
||||
event_id = generate_fake_event_id()
|
||||
finally:
|
||||
# TODO: If this is a temporary error, retry. Otherwise, consider notifying clients of the failure
|
||||
try:
|
||||
await self._store.delete_processed_delayed_event(
|
||||
event.delay_id, event.user_localpart
|
||||
)
|
||||
except Exception:
|
||||
logger.exception("Failed to delete processed delayed event")
|
||||
|
||||
set_tag("event_id", event_id)
|
||||
|
||||
def _get_current_ts(self) -> Timestamp:
|
||||
return Timestamp(self._clock.time_msec())
|
||||
|
||||
def _next_send_ts_changed(self, next_send_ts: Optional[Timestamp]) -> bool:
|
||||
# The DB alone knows if the next send time changed after adding/modifying
|
||||
# a delayed event, but if we were to ever miss updating our delayed call's
|
||||
# firing time, we may miss other updates. So, keep track of changes to the
|
||||
# the next send time here instead of in the DB.
|
||||
cached_next_send_ts = (
|
||||
int(self._next_delayed_event_call.getTime() * 1000)
|
||||
if self._next_delayed_event_call is not None
|
||||
else None
|
||||
)
|
||||
return next_send_ts != cached_next_send_ts
|
||||
@@ -48,6 +48,7 @@ from synapse.metrics.background_process_metrics import (
|
||||
wrap_as_background_process,
|
||||
)
|
||||
from synapse.storage.databases.main.client_ips import DeviceLastConnectionInfo
|
||||
from synapse.storage.databases.main.roommember import EventIdMembership
|
||||
from synapse.storage.databases.main.state_deltas import StateDelta
|
||||
from synapse.types import (
|
||||
DeviceListUpdates,
|
||||
@@ -222,7 +223,6 @@ class DeviceWorkerHandler:
|
||||
return changed
|
||||
|
||||
@trace
|
||||
@measure_func("device.get_user_ids_changed")
|
||||
@cancellable
|
||||
async def get_user_ids_changed(
|
||||
self, user_id: str, from_token: StreamToken
|
||||
@@ -290,9 +290,11 @@ class DeviceWorkerHandler:
|
||||
memberships_to_fetch.add(delta.prev_event_id)
|
||||
|
||||
# Fetch all the memberships for the membership events
|
||||
event_id_to_memberships = await self.store.get_membership_from_event_ids(
|
||||
memberships_to_fetch
|
||||
)
|
||||
event_id_to_memberships: Mapping[str, Optional[EventIdMembership]] = {}
|
||||
if memberships_to_fetch:
|
||||
event_id_to_memberships = await self.store.get_membership_from_event_ids(
|
||||
memberships_to_fetch
|
||||
)
|
||||
|
||||
joined_invited_knocked = (
|
||||
Membership.JOIN,
|
||||
@@ -349,7 +351,6 @@ class DeviceWorkerHandler:
|
||||
|
||||
return device_list_updates
|
||||
|
||||
@measure_func("_generate_sync_entry_for_device_list")
|
||||
async def generate_sync_entry_for_device_list(
|
||||
self,
|
||||
user_id: str,
|
||||
|
||||
@@ -18,7 +18,7 @@
|
||||
# [This file includes modifications made by New Vector Limited]
|
||||
#
|
||||
#
|
||||
from typing import TYPE_CHECKING
|
||||
from typing import TYPE_CHECKING, Optional, Tuple
|
||||
|
||||
from authlib.jose import JsonWebToken, JWTClaims
|
||||
from authlib.jose.errors import BadSignatureError, InvalidClaimError, JoseError
|
||||
@@ -36,11 +36,12 @@ class JwtHandler:
|
||||
|
||||
self.jwt_secret = hs.config.jwt.jwt_secret
|
||||
self.jwt_subject_claim = hs.config.jwt.jwt_subject_claim
|
||||
self.jwt_display_name_claim = hs.config.jwt.jwt_display_name_claim
|
||||
self.jwt_algorithm = hs.config.jwt.jwt_algorithm
|
||||
self.jwt_issuer = hs.config.jwt.jwt_issuer
|
||||
self.jwt_audiences = hs.config.jwt.jwt_audiences
|
||||
|
||||
def validate_login(self, login_submission: JsonDict) -> str:
|
||||
def validate_login(self, login_submission: JsonDict) -> Tuple[str, Optional[str]]:
|
||||
"""
|
||||
Authenticates the user for the /login API
|
||||
|
||||
@@ -49,7 +50,8 @@ class JwtHandler:
|
||||
(including 'type' and other relevant fields)
|
||||
|
||||
Returns:
|
||||
The user ID that is logging in.
|
||||
A tuple of (user_id, display_name) of the user that is logging in.
|
||||
If the JWT does not contain a display name, the second element of the tuple will be None.
|
||||
|
||||
Raises:
|
||||
LoginError if there was an authentication problem.
|
||||
@@ -109,4 +111,10 @@ class JwtHandler:
|
||||
if user is None:
|
||||
raise LoginError(403, "Invalid JWT", errcode=Codes.FORBIDDEN)
|
||||
|
||||
return UserID(user, self.hs.hostname).to_string()
|
||||
default_display_name = None
|
||||
if self.jwt_display_name_claim:
|
||||
display_name_claim = claims.get(self.jwt_display_name_claim)
|
||||
if display_name_claim is not None:
|
||||
default_display_name = display_name_claim
|
||||
|
||||
return UserID(user, self.hs.hostname).to_string(), default_display_name
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
|
||||
import logging
|
||||
from itertools import chain
|
||||
from typing import TYPE_CHECKING, Dict, List, Mapping, Optional, Set, Tuple
|
||||
from typing import TYPE_CHECKING, AbstractSet, Dict, List, Mapping, Optional, Set, Tuple
|
||||
|
||||
from prometheus_client import Histogram
|
||||
from typing_extensions import assert_never
|
||||
@@ -49,6 +49,7 @@ from synapse.types import (
|
||||
Requester,
|
||||
SlidingSyncStreamToken,
|
||||
StateMap,
|
||||
StrCollection,
|
||||
StreamKeyType,
|
||||
StreamToken,
|
||||
)
|
||||
@@ -267,7 +268,7 @@ class SlidingSyncHandler:
|
||||
|
||||
if relevant_rooms_to_send_map:
|
||||
with start_active_span("sliding_sync.generate_room_entries"):
|
||||
await concurrently_execute(handle_room, relevant_rooms_to_send_map, 10)
|
||||
await concurrently_execute(handle_room, relevant_rooms_to_send_map, 20)
|
||||
|
||||
extensions = await self.extensions.get_extensions_response(
|
||||
sync_config=sync_config,
|
||||
@@ -293,7 +294,6 @@ class SlidingSyncHandler:
|
||||
# to record rooms as having updates even if there might not actually
|
||||
# be anything new for the user (e.g. due to event filters, events
|
||||
# having happened after the user left, etc).
|
||||
unsent_room_ids = []
|
||||
if from_token:
|
||||
# The set of rooms that the client (may) care about, but aren't
|
||||
# in any list range (or subscribed to).
|
||||
@@ -305,15 +305,24 @@ class SlidingSyncHandler:
|
||||
# TODO: Replace this with something faster. When we land the
|
||||
# sliding sync tables that record the most recent event
|
||||
# positions we can use that.
|
||||
missing_event_map_by_room = (
|
||||
await self.store.get_room_events_stream_for_rooms(
|
||||
room_ids=missing_rooms,
|
||||
from_key=to_token.room_key,
|
||||
to_key=from_token.stream_token.room_key,
|
||||
limit=1,
|
||||
unsent_room_ids: StrCollection
|
||||
if await self.store.have_finished_sliding_sync_background_jobs():
|
||||
unsent_room_ids = await (
|
||||
self.store.get_rooms_that_have_updates_since_sliding_sync_table(
|
||||
room_ids=missing_rooms,
|
||||
from_key=from_token.stream_token.room_key,
|
||||
)
|
||||
)
|
||||
)
|
||||
unsent_room_ids = list(missing_event_map_by_room)
|
||||
else:
|
||||
missing_event_map_by_room = (
|
||||
await self.store.get_room_events_stream_for_rooms(
|
||||
room_ids=missing_rooms,
|
||||
from_key=to_token.room_key,
|
||||
to_key=from_token.stream_token.room_key,
|
||||
limit=1,
|
||||
)
|
||||
)
|
||||
unsent_room_ids = list(missing_event_map_by_room)
|
||||
|
||||
new_connection_state.rooms.record_unsent_rooms(
|
||||
unsent_room_ids, from_token.stream_token.room_key
|
||||
@@ -443,13 +452,11 @@ class SlidingSyncHandler:
|
||||
to_token=to_token,
|
||||
)
|
||||
|
||||
event_map = await self.store.get_events(list(state_ids.values()))
|
||||
events = await self.store.get_events_as_list(list(state_ids.values()))
|
||||
|
||||
state_map = {}
|
||||
for key, event_id in state_ids.items():
|
||||
event = event_map.get(event_id)
|
||||
if event:
|
||||
state_map[key] = event
|
||||
for event in events:
|
||||
state_map[(event.type, event.state_key)] = event
|
||||
|
||||
return state_map
|
||||
|
||||
@@ -495,6 +502,26 @@ class SlidingSyncHandler:
|
||||
room_sync_config.timeline_limit,
|
||||
)
|
||||
|
||||
# Handle state resets. For example, if we see
|
||||
# `room_membership_for_user_at_to_token.event_id=None and
|
||||
# room_membership_for_user_at_to_token.membership is not None`, we should
|
||||
# indicate to the client that a state reset happened. Perhaps we should indicate
|
||||
# this by setting `initial: True` and empty `required_state: []`.
|
||||
state_reset_out_of_room = False
|
||||
if (
|
||||
room_membership_for_user_at_to_token.event_id is None
|
||||
and room_membership_for_user_at_to_token.membership is not None
|
||||
):
|
||||
# We only expect the `event_id` to be `None` if you've been state reset out
|
||||
# of the room (meaning you're no longer in the room). We could put this as
|
||||
# part of the if-statement above but we want to handle every case where
|
||||
# `event_id` is `None`.
|
||||
assert room_membership_for_user_at_to_token.membership is Membership.LEAVE
|
||||
|
||||
state_reset_out_of_room = True
|
||||
|
||||
prev_room_sync_config = previous_connection_state.room_configs.get(room_id)
|
||||
|
||||
# Determine whether we should limit the timeline to the token range.
|
||||
#
|
||||
# We should return historical messages (before token range) in the
|
||||
@@ -523,11 +550,10 @@ class SlidingSyncHandler:
|
||||
# or `limited` mean for clients that interpret them correctly. In future this
|
||||
# behavior is almost certainly going to change.
|
||||
#
|
||||
# TODO: Also handle changes to `required_state`
|
||||
from_bound = None
|
||||
initial = True
|
||||
ignore_timeline_bound = False
|
||||
if from_token and not newly_joined:
|
||||
if from_token and not newly_joined and not state_reset_out_of_room:
|
||||
room_status = previous_connection_state.rooms.have_sent_room(room_id)
|
||||
if room_status.status == HaveSentRoomFlag.LIVE:
|
||||
from_bound = from_token.stream_token.room_key
|
||||
@@ -544,7 +570,6 @@ class SlidingSyncHandler:
|
||||
|
||||
log_kv({"sliding_sync.room_status": room_status})
|
||||
|
||||
prev_room_sync_config = previous_connection_state.room_configs.get(room_id)
|
||||
if prev_room_sync_config is not None:
|
||||
# Check if the timeline limit has increased, if so ignore the
|
||||
# timeline bound and record the change (see "XXX: Odd behavior"
|
||||
@@ -555,8 +580,6 @@ class SlidingSyncHandler:
|
||||
):
|
||||
ignore_timeline_bound = True
|
||||
|
||||
# TODO: Check for changes in `required_state``
|
||||
|
||||
log_kv(
|
||||
{
|
||||
"sliding_sync.from_bound": from_bound,
|
||||
@@ -732,12 +755,6 @@ class SlidingSyncHandler:
|
||||
|
||||
stripped_state.append(strip_event(invite_or_knock_event))
|
||||
|
||||
# TODO: Handle state resets. For example, if we see
|
||||
# `room_membership_for_user_at_to_token.event_id=None and
|
||||
# room_membership_for_user_at_to_token.membership is not None`, we should
|
||||
# indicate to the client that a state reset happened. Perhaps we should indicate
|
||||
# this by setting `initial: True` and empty `required_state`.
|
||||
|
||||
# Get the changes to current state in the token range from the
|
||||
# `current_state_delta_stream` table.
|
||||
#
|
||||
@@ -784,32 +801,10 @@ class SlidingSyncHandler:
|
||||
):
|
||||
avatar_changed = True
|
||||
|
||||
# We only need the room summary for calculating heroes, however if we do
|
||||
# fetch it then we can use it to calculate `joined_count` and
|
||||
# `invited_count`.
|
||||
room_membership_summary: Optional[Mapping[str, MemberSummary]] = None
|
||||
empty_membership_summary = MemberSummary([], 0)
|
||||
# We need the room summary for:
|
||||
# - Always for initial syncs (or the first time we send down the room)
|
||||
# - When the room has no name, we need `heroes`
|
||||
# - When the membership has changed so we need to give updated `heroes` and
|
||||
# `joined_count`/`invited_count`.
|
||||
#
|
||||
# Ideally, instead of just looking at `name_changed`, we'd check if the room
|
||||
# name is not set but this is a good enough approximation that saves us from
|
||||
# having to pull out the full event. This just means, we're generating the
|
||||
# summary whenever the room name changes instead of only when it changes to
|
||||
# `None`.
|
||||
if initial or name_changed or membership_changed:
|
||||
# We can't trace the function directly because it's cached and the `@cached`
|
||||
# decorator doesn't mix with `@trace` yet.
|
||||
with start_active_span("get_room_summary"):
|
||||
if room_membership_for_user_at_to_token.membership in (
|
||||
Membership.LEAVE,
|
||||
Membership.BAN,
|
||||
):
|
||||
# TODO: Figure out how to get the membership summary for left/banned rooms
|
||||
room_membership_summary = {}
|
||||
else:
|
||||
room_membership_summary = await self.store.get_room_summary(room_id)
|
||||
# TODO: Reverse/rewind back to the `to_token`
|
||||
|
||||
# `heroes` are required if the room name is not set.
|
||||
#
|
||||
@@ -828,11 +823,45 @@ class SlidingSyncHandler:
|
||||
# get them on initial syncs (or the first time we send down the room) or if the
|
||||
# membership has changed which may change the heroes.
|
||||
if name_event_id is None and (initial or (not initial and membership_changed)):
|
||||
assert room_membership_summary is not None
|
||||
# We need the room summary to extract the heroes from
|
||||
if room_membership_for_user_at_to_token.membership != Membership.JOIN:
|
||||
# TODO: Figure out how to get the membership summary for left/banned rooms
|
||||
# For invite/knock rooms we don't include the information.
|
||||
room_membership_summary = {}
|
||||
else:
|
||||
room_membership_summary = await self.store.get_room_summary(room_id)
|
||||
# TODO: Reverse/rewind back to the `to_token`
|
||||
|
||||
hero_user_ids = extract_heroes_from_room_summary(
|
||||
room_membership_summary, me=user.to_string()
|
||||
)
|
||||
|
||||
# Fetch the membership counts for rooms we're joined to.
|
||||
#
|
||||
# Similarly to other metadata, we only need to calculate the member
|
||||
# counts if this is an initial sync or the memberships have changed.
|
||||
joined_count: Optional[int] = None
|
||||
invited_count: Optional[int] = None
|
||||
if (
|
||||
initial or membership_changed
|
||||
) and room_membership_for_user_at_to_token.membership == Membership.JOIN:
|
||||
# If we have the room summary (because we calculated heroes above)
|
||||
# then we can simply pull the counts from there.
|
||||
if room_membership_summary is not None:
|
||||
empty_membership_summary = MemberSummary([], 0)
|
||||
|
||||
joined_count = room_membership_summary.get(
|
||||
Membership.JOIN, empty_membership_summary
|
||||
).count
|
||||
|
||||
invited_count = room_membership_summary.get(
|
||||
Membership.INVITE, empty_membership_summary
|
||||
).count
|
||||
else:
|
||||
member_counts = await self.store.get_member_counts(room_id)
|
||||
joined_count = member_counts.get(Membership.JOIN, 0)
|
||||
invited_count = member_counts.get(Membership.INVITE, 0)
|
||||
|
||||
# Fetch the `required_state` for the room
|
||||
#
|
||||
# No `required_state` for invite/knock rooms (just `stripped_state`)
|
||||
@@ -964,6 +993,10 @@ class SlidingSyncHandler:
|
||||
include_others=required_state_filter.include_others,
|
||||
)
|
||||
|
||||
# The required state map to store in the room sync config, if it has
|
||||
# changed.
|
||||
changed_required_state_map: Optional[Mapping[str, AbstractSet[str]]] = None
|
||||
|
||||
# We can return all of the state that was requested if this was the first
|
||||
# time we've sent the room down this connection.
|
||||
room_state: StateMap[EventBase] = {}
|
||||
@@ -977,6 +1010,29 @@ class SlidingSyncHandler:
|
||||
else:
|
||||
assert from_bound is not None
|
||||
|
||||
if prev_room_sync_config is not None:
|
||||
# Check if there are any changes to the required state config
|
||||
# that we need to handle.
|
||||
changed_required_state_map, added_state_filter = (
|
||||
_required_state_changes(
|
||||
user.to_string(),
|
||||
previous_room_config=prev_room_sync_config,
|
||||
room_sync_config=room_sync_config,
|
||||
state_deltas=room_state_delta_id_map,
|
||||
)
|
||||
)
|
||||
|
||||
if added_state_filter:
|
||||
# Some state entries got added, so we pull out the current
|
||||
# state for them. If we don't do this we'd only send down new deltas.
|
||||
state_ids = await self.get_current_state_ids_at(
|
||||
room_id=room_id,
|
||||
room_membership_for_user_at_to_token=room_membership_for_user_at_to_token,
|
||||
state_filter=added_state_filter,
|
||||
to_token=to_token,
|
||||
)
|
||||
room_state_delta_id_map.update(state_ids)
|
||||
|
||||
events = await self.store.get_events(
|
||||
state_filter.filter_state(room_state_delta_id_map).values()
|
||||
)
|
||||
@@ -1024,25 +1080,64 @@ class SlidingSyncHandler:
|
||||
)
|
||||
)
|
||||
|
||||
# Figure out the last bump event in the room
|
||||
#
|
||||
# By default, just choose the membership event position for any non-join membership
|
||||
bump_stamp = room_membership_for_user_at_to_token.event_pos.stream
|
||||
# Figure out the last bump event in the room. If the bump stamp hasn't
|
||||
# changed we omit it from the response.
|
||||
bump_stamp = None
|
||||
|
||||
always_return_bump_stamp = (
|
||||
# We use the membership event position for any non-join
|
||||
room_membership_for_user_at_to_token.membership != Membership.JOIN
|
||||
# We didn't fetch any timeline events but we should still check for
|
||||
# a bump_stamp that might be somewhere
|
||||
or limited is None
|
||||
# There might be a bump event somewhere before the timeline events
|
||||
# that we fetched, that we didn't previously send down
|
||||
or limited is True
|
||||
# Always give the client some frame of reference if this is the
|
||||
# first time they are seeing the room down the connection
|
||||
or initial
|
||||
)
|
||||
|
||||
# If we're joined to the room, we need to find the last bump event before the
|
||||
# `to_token`
|
||||
if room_membership_for_user_at_to_token.membership == Membership.JOIN:
|
||||
# Try and get a bump stamp, if not we just fall back to the
|
||||
# membership token.
|
||||
# Try and get a bump stamp
|
||||
new_bump_stamp = await self._get_bump_stamp(
|
||||
room_id, to_token, timeline_events
|
||||
room_id,
|
||||
to_token,
|
||||
timeline_events,
|
||||
check_outside_timeline=always_return_bump_stamp,
|
||||
)
|
||||
if new_bump_stamp is not None:
|
||||
bump_stamp = new_bump_stamp
|
||||
|
||||
unstable_expanded_timeline = False
|
||||
prev_room_sync_config = previous_connection_state.room_configs.get(room_id)
|
||||
if bump_stamp is None and always_return_bump_stamp:
|
||||
# By default, just choose the membership event position for any non-join membership
|
||||
bump_stamp = room_membership_for_user_at_to_token.event_pos.stream
|
||||
|
||||
if bump_stamp is not None and bump_stamp < 0:
|
||||
# We never want to send down negative stream orderings, as you can't
|
||||
# sensibly compare positive and negative stream orderings (they have
|
||||
# different meanings).
|
||||
#
|
||||
# A negative bump stamp here can only happen if the stream ordering
|
||||
# of the membership event is negative (and there are no further bump
|
||||
# stamps), which can happen if the server leaves and deletes a room,
|
||||
# and then rejoins it.
|
||||
#
|
||||
# To deal with this, we just set the bump stamp to zero, which will
|
||||
# shove this room to the bottom of the list. This is OK as the
|
||||
# moment a new message happens in the room it will get put into a
|
||||
# sensible order again.
|
||||
bump_stamp = 0
|
||||
|
||||
room_sync_required_state_map_to_persist = room_sync_config.required_state_map
|
||||
if changed_required_state_map:
|
||||
room_sync_required_state_map_to_persist = changed_required_state_map
|
||||
|
||||
# Record the `room_sync_config` if we're `ignore_timeline_bound` (which means
|
||||
# that the `timeline_limit` has increased)
|
||||
unstable_expanded_timeline = False
|
||||
if ignore_timeline_bound:
|
||||
# FIXME: We signal the fact that we're sending down more events to
|
||||
# the client by setting `unstable_expanded_timeline` to true (see
|
||||
@@ -1051,7 +1146,7 @@ class SlidingSyncHandler:
|
||||
|
||||
new_connection_state.room_configs[room_id] = RoomSyncConfig(
|
||||
timeline_limit=room_sync_config.timeline_limit,
|
||||
required_state_map=room_sync_config.required_state_map,
|
||||
required_state_map=room_sync_required_state_map_to_persist,
|
||||
)
|
||||
elif prev_room_sync_config is not None:
|
||||
# If the result is `limited` then we need to record that the
|
||||
@@ -1080,30 +1175,20 @@ class SlidingSyncHandler:
|
||||
):
|
||||
new_connection_state.room_configs[room_id] = RoomSyncConfig(
|
||||
timeline_limit=room_sync_config.timeline_limit,
|
||||
required_state_map=room_sync_config.required_state_map,
|
||||
required_state_map=room_sync_required_state_map_to_persist,
|
||||
)
|
||||
|
||||
# TODO: Record changes in required_state.
|
||||
elif changed_required_state_map is not None:
|
||||
new_connection_state.room_configs[room_id] = RoomSyncConfig(
|
||||
timeline_limit=room_sync_config.timeline_limit,
|
||||
required_state_map=room_sync_required_state_map_to_persist,
|
||||
)
|
||||
|
||||
else:
|
||||
new_connection_state.room_configs[room_id] = room_sync_config
|
||||
|
||||
set_tag(SynapseTags.RESULT_PREFIX + "initial", initial)
|
||||
|
||||
joined_count: Optional[int] = None
|
||||
if initial or membership_changed:
|
||||
assert room_membership_summary is not None
|
||||
joined_count = room_membership_summary.get(
|
||||
Membership.JOIN, empty_membership_summary
|
||||
).count
|
||||
|
||||
invited_count: Optional[int] = None
|
||||
if initial or membership_changed:
|
||||
assert room_membership_summary is not None
|
||||
invited_count = room_membership_summary.get(
|
||||
Membership.INVITE, empty_membership_summary
|
||||
).count
|
||||
|
||||
return SlidingSyncResult.RoomResult(
|
||||
name=room_name,
|
||||
avatar=room_avatar,
|
||||
@@ -1130,14 +1215,23 @@ class SlidingSyncHandler:
|
||||
|
||||
@trace
|
||||
async def _get_bump_stamp(
|
||||
self, room_id: str, to_token: StreamToken, timeline: List[EventBase]
|
||||
self,
|
||||
room_id: str,
|
||||
to_token: StreamToken,
|
||||
timeline: List[EventBase],
|
||||
check_outside_timeline: bool,
|
||||
) -> Optional[int]:
|
||||
"""Get a bump stamp for the room, if we have a bump event
|
||||
"""Get a bump stamp for the room, if we have a bump event and it has
|
||||
changed.
|
||||
|
||||
Args:
|
||||
room_id
|
||||
to_token: The upper bound of token to return
|
||||
timeline: The list of events we have fetched.
|
||||
limited: If the timeline was limited.
|
||||
check_outside_timeline: Whether we need to check for bump stamp for
|
||||
events before the timeline if we didn't find a bump stamp in
|
||||
the timeline events.
|
||||
"""
|
||||
|
||||
# First check the timeline events we're returning to see if one of
|
||||
@@ -1157,6 +1251,11 @@ class SlidingSyncHandler:
|
||||
if new_bump_stamp > 0:
|
||||
return new_bump_stamp
|
||||
|
||||
if not check_outside_timeline:
|
||||
# If we are not a limited sync, then we know the bump stamp can't
|
||||
# have changed.
|
||||
return None
|
||||
|
||||
# We can quickly query for the latest bump event in the room using the
|
||||
# sliding sync tables.
|
||||
latest_room_bump_stamp = await self.store.get_latest_bump_stamp_for_room(
|
||||
@@ -1173,8 +1272,8 @@ class SlidingSyncHandler:
|
||||
# `SCHEMA_COMPAT_VERSION` and run the foreground update for
|
||||
# `sliding_sync_joined_rooms`/`sliding_sync_membership_snapshots`
|
||||
# (tracked by https://github.com/element-hq/synapse/issues/17623)
|
||||
await self.store.have_finished_sliding_sync_background_jobs()
|
||||
and latest_room_bump_stamp is None
|
||||
latest_room_bump_stamp is None
|
||||
and await self.store.have_finished_sliding_sync_background_jobs()
|
||||
):
|
||||
return None
|
||||
|
||||
@@ -1216,3 +1315,185 @@ class SlidingSyncHandler:
|
||||
return new_bump_event_pos.stream
|
||||
|
||||
return None
|
||||
|
||||
|
||||
def _required_state_changes(
|
||||
user_id: str,
|
||||
*,
|
||||
previous_room_config: "RoomSyncConfig",
|
||||
room_sync_config: RoomSyncConfig,
|
||||
state_deltas: StateMap[str],
|
||||
) -> Tuple[Optional[Mapping[str, AbstractSet[str]]], StateFilter]:
|
||||
"""Calculates the changes between the required state room config from the
|
||||
previous requests compared with the current request.
|
||||
|
||||
This does two things. First, it calculates if we need to update the room
|
||||
config due to changes to required state. Secondly, it works out which state
|
||||
entries we need to pull from current state and return due to the state entry
|
||||
now appearing in the required state when it previously wasn't (on top of the
|
||||
state deltas).
|
||||
|
||||
This function tries to ensure to handle the case where a state entry is
|
||||
added, removed and then added again to the required state. In that case we
|
||||
only want to re-send that entry down sync if it has changed.
|
||||
|
||||
Returns:
|
||||
A 2-tuple of updated required state config (or None if there is no update)
|
||||
and the state filter to use to fetch extra current state that we need to
|
||||
return.
|
||||
"""
|
||||
|
||||
prev_required_state_map = previous_room_config.required_state_map
|
||||
request_required_state_map = room_sync_config.required_state_map
|
||||
|
||||
if prev_required_state_map == request_required_state_map:
|
||||
# There has been no change. Return immediately.
|
||||
return None, StateFilter.none()
|
||||
|
||||
prev_wildcard = prev_required_state_map.get(StateValues.WILDCARD, set())
|
||||
request_wildcard = request_required_state_map.get(StateValues.WILDCARD, set())
|
||||
|
||||
# If we were previously fetching everything ("*", "*"), always update the effective
|
||||
# room required state config to match the request. And since we we're previously
|
||||
# already fetching everything, we don't have to fetch anything now that they've
|
||||
# narrowed.
|
||||
if StateValues.WILDCARD in prev_wildcard:
|
||||
return request_required_state_map, StateFilter.none()
|
||||
|
||||
# If a event type wildcard has been added or removed we don't try and do
|
||||
# anything fancy, and instead always update the effective room required
|
||||
# state config to match the request.
|
||||
if request_wildcard - prev_wildcard:
|
||||
# Some keys were added, so we need to fetch everything
|
||||
return request_required_state_map, StateFilter.all()
|
||||
if prev_wildcard - request_wildcard:
|
||||
# Keys were only removed, so we don't have to fetch everything.
|
||||
return request_required_state_map, StateFilter.none()
|
||||
|
||||
# Contains updates to the required state map compared with the previous room
|
||||
# config. This has the same format as `RoomSyncConfig.required_state`
|
||||
changes: Dict[str, AbstractSet[str]] = {}
|
||||
|
||||
# The set of types/state keys that we need to fetch and return to the
|
||||
# client. Passed to `StateFilter.from_types(...)`
|
||||
added: List[Tuple[str, Optional[str]]] = []
|
||||
|
||||
# First we calculate what, if anything, has been *added*.
|
||||
for event_type in (
|
||||
prev_required_state_map.keys() | request_required_state_map.keys()
|
||||
):
|
||||
old_state_keys = prev_required_state_map.get(event_type, set())
|
||||
request_state_keys = request_required_state_map.get(event_type, set())
|
||||
|
||||
if old_state_keys == request_state_keys:
|
||||
# No change to this type
|
||||
continue
|
||||
|
||||
if not request_state_keys - old_state_keys:
|
||||
# Nothing *added*, so we skip. Removals happen below.
|
||||
continue
|
||||
|
||||
# Always update changes to include the newly added keys
|
||||
changes[event_type] = request_state_keys
|
||||
|
||||
if StateValues.WILDCARD in old_state_keys:
|
||||
# We were previously fetching everything for this type, so we don't need to
|
||||
# fetch anything new.
|
||||
continue
|
||||
|
||||
# Record the new state keys to fetch for this type.
|
||||
if StateValues.WILDCARD in request_state_keys:
|
||||
# If we have added a wildcard then we always just fetch everything.
|
||||
added.append((event_type, None))
|
||||
else:
|
||||
for state_key in request_state_keys - old_state_keys:
|
||||
if state_key == StateValues.ME:
|
||||
added.append((event_type, user_id))
|
||||
elif state_key == StateValues.LAZY:
|
||||
# We handle lazy loading separately (outside this function),
|
||||
# so don't need to explicitly add anything here.
|
||||
#
|
||||
# LAZY values should also be ignore for event types that are
|
||||
# not membership.
|
||||
pass
|
||||
else:
|
||||
added.append((event_type, state_key))
|
||||
|
||||
added_state_filter = StateFilter.from_types(added)
|
||||
|
||||
# Convert the list of state deltas to map from type to state_keys that have
|
||||
# changed.
|
||||
changed_types_to_state_keys: Dict[str, Set[str]] = {}
|
||||
for event_type, state_key in state_deltas:
|
||||
changed_types_to_state_keys.setdefault(event_type, set()).add(state_key)
|
||||
|
||||
# Figure out what changes we need to apply to the effective required state
|
||||
# config.
|
||||
for event_type, changed_state_keys in changed_types_to_state_keys.items():
|
||||
old_state_keys = prev_required_state_map.get(event_type, set())
|
||||
request_state_keys = request_required_state_map.get(event_type, set())
|
||||
|
||||
if old_state_keys == request_state_keys:
|
||||
# No change.
|
||||
continue
|
||||
|
||||
if request_state_keys - old_state_keys:
|
||||
# We've expanded the set of state keys, so we just clobber the
|
||||
# current set with the new set.
|
||||
#
|
||||
# We could also ensure that we keep entries where the state hasn't
|
||||
# changed, but are no longer in the requested required state, but
|
||||
# that's a sufficient edge case that we can ignore (as its only a
|
||||
# performance optimization).
|
||||
changes[event_type] = request_state_keys
|
||||
continue
|
||||
|
||||
old_state_key_wildcard = StateValues.WILDCARD in old_state_keys
|
||||
request_state_key_wildcard = StateValues.WILDCARD in request_state_keys
|
||||
|
||||
if old_state_key_wildcard != request_state_key_wildcard:
|
||||
# If a state_key wildcard has been added or removed, we always update the
|
||||
# effective room required state config to match the request.
|
||||
changes[event_type] = request_state_keys
|
||||
continue
|
||||
|
||||
if event_type == EventTypes.Member:
|
||||
old_state_key_lazy = StateValues.LAZY in old_state_keys
|
||||
request_state_key_lazy = StateValues.LAZY in request_state_keys
|
||||
|
||||
if old_state_key_lazy != request_state_key_lazy:
|
||||
# If a "$LAZY" has been added or removed we always update the effective room
|
||||
# required state config to match the request.
|
||||
changes[event_type] = request_state_keys
|
||||
continue
|
||||
|
||||
# Handle "$ME" values by adding "$ME" if the state key matches the user
|
||||
# ID.
|
||||
if user_id in changed_state_keys:
|
||||
changed_state_keys.add(StateValues.ME)
|
||||
|
||||
# At this point there are no wildcards and no additions to the set of
|
||||
# state keys requested, only deletions.
|
||||
#
|
||||
# We only remove state keys from the effective state if they've been
|
||||
# removed from the request *and* the state has changed. This ensures
|
||||
# that if a client removes and then re-adds a state key, we only send
|
||||
# down the associated current state event if its changed (rather than
|
||||
# sending down the same event twice).
|
||||
invalidated = (old_state_keys - request_state_keys) & changed_state_keys
|
||||
if invalidated:
|
||||
changes[event_type] = old_state_keys - invalidated
|
||||
|
||||
if changes:
|
||||
# Update the required state config based on the changes.
|
||||
new_required_state_map = dict(prev_required_state_map)
|
||||
for event_type, state_keys in changes.items():
|
||||
if state_keys:
|
||||
new_required_state_map[event_type] = state_keys
|
||||
else:
|
||||
# Remove entries with empty state keys.
|
||||
new_required_state_map.pop(event_type, None)
|
||||
|
||||
return new_required_state_map, added_state_filter
|
||||
else:
|
||||
return None, added_state_filter
|
||||
|
||||
@@ -14,7 +14,18 @@
|
||||
|
||||
import itertools
|
||||
import logging
|
||||
from typing import TYPE_CHECKING, AbstractSet, Dict, Mapping, Optional, Sequence, Set
|
||||
from typing import (
|
||||
TYPE_CHECKING,
|
||||
AbstractSet,
|
||||
ChainMap,
|
||||
Dict,
|
||||
Mapping,
|
||||
MutableMapping,
|
||||
Optional,
|
||||
Sequence,
|
||||
Set,
|
||||
cast,
|
||||
)
|
||||
|
||||
from typing_extensions import assert_never
|
||||
|
||||
@@ -38,6 +49,7 @@ from synapse.types.handlers.sliding_sync import (
|
||||
SlidingSyncConfig,
|
||||
SlidingSyncResult,
|
||||
)
|
||||
from synapse.util.async_helpers import concurrently_execute
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from synapse.server import HomeServer
|
||||
@@ -106,6 +118,8 @@ class SlidingSyncExtensionHandler:
|
||||
if sync_config.extensions.account_data is not None:
|
||||
account_data_response = await self.get_account_data_extension_response(
|
||||
sync_config=sync_config,
|
||||
previous_connection_state=previous_connection_state,
|
||||
new_connection_state=new_connection_state,
|
||||
actual_lists=actual_lists,
|
||||
actual_room_ids=actual_room_ids,
|
||||
account_data_request=sync_config.extensions.account_data,
|
||||
@@ -348,6 +362,8 @@ class SlidingSyncExtensionHandler:
|
||||
async def get_account_data_extension_response(
|
||||
self,
|
||||
sync_config: SlidingSyncConfig,
|
||||
previous_connection_state: "PerConnectionState",
|
||||
new_connection_state: "MutablePerConnectionState",
|
||||
actual_lists: Mapping[str, SlidingSyncResult.SlidingWindowList],
|
||||
actual_room_ids: Set[str],
|
||||
account_data_request: SlidingSyncConfig.Extensions.AccountDataExtension,
|
||||
@@ -380,29 +396,39 @@ class SlidingSyncExtensionHandler:
|
||||
)
|
||||
)
|
||||
|
||||
# TODO: This should take into account the `from_token` and `to_token`
|
||||
have_push_rules_changed = await self.store.have_push_rules_changed_for_user(
|
||||
user_id, from_token.stream_token.push_rules_key
|
||||
)
|
||||
if have_push_rules_changed:
|
||||
global_account_data_map = dict(global_account_data_map)
|
||||
# TODO: This should take into account the `from_token` and `to_token`
|
||||
global_account_data_map[
|
||||
AccountDataTypes.PUSH_RULES
|
||||
] = await self.push_rules_handler.push_rules_for_user(sync_config.user)
|
||||
else:
|
||||
# TODO: This should take into account the `to_token`
|
||||
all_global_account_data = await self.store.get_global_account_data_for_user(
|
||||
user_id
|
||||
immutable_global_account_data_map = (
|
||||
await self.store.get_global_account_data_for_user(user_id)
|
||||
)
|
||||
|
||||
global_account_data_map = dict(all_global_account_data)
|
||||
# TODO: This should take into account the `to_token`
|
||||
global_account_data_map[
|
||||
AccountDataTypes.PUSH_RULES
|
||||
] = await self.push_rules_handler.push_rules_for_user(sync_config.user)
|
||||
# Use a `ChainMap` to avoid copying the immutable data from the cache
|
||||
global_account_data_map = ChainMap(
|
||||
{
|
||||
# TODO: This should take into account the `to_token`
|
||||
AccountDataTypes.PUSH_RULES: await self.push_rules_handler.push_rules_for_user(
|
||||
sync_config.user
|
||||
)
|
||||
},
|
||||
# Cast is safe because `ChainMap` only mutates the top-most map,
|
||||
# see https://github.com/python/typeshed/issues/8430
|
||||
cast(
|
||||
MutableMapping[str, JsonMapping], immutable_global_account_data_map
|
||||
),
|
||||
)
|
||||
|
||||
# Fetch room account data
|
||||
account_data_by_room_map: Mapping[str, Mapping[str, JsonMapping]] = {}
|
||||
#
|
||||
account_data_by_room_map: MutableMapping[str, Mapping[str, JsonMapping]] = {}
|
||||
relevant_room_ids = self.find_relevant_room_ids_for_extension(
|
||||
requested_lists=account_data_request.lists,
|
||||
requested_room_ids=account_data_request.rooms,
|
||||
@@ -410,25 +436,154 @@ class SlidingSyncExtensionHandler:
|
||||
actual_room_ids=actual_room_ids,
|
||||
)
|
||||
if len(relevant_room_ids) > 0:
|
||||
# We need to handle the different cases depending on if we have sent
|
||||
# down account data previously or not, so we split the relevant
|
||||
# rooms up into different collections based on status.
|
||||
live_rooms = set()
|
||||
previously_rooms: Dict[str, int] = {}
|
||||
initial_rooms = set()
|
||||
|
||||
for room_id in relevant_room_ids:
|
||||
if not from_token:
|
||||
initial_rooms.add(room_id)
|
||||
continue
|
||||
|
||||
room_status = previous_connection_state.account_data.have_sent_room(
|
||||
room_id
|
||||
)
|
||||
if room_status.status == HaveSentRoomFlag.LIVE:
|
||||
live_rooms.add(room_id)
|
||||
elif room_status.status == HaveSentRoomFlag.PREVIOUSLY:
|
||||
assert room_status.last_token is not None
|
||||
previously_rooms[room_id] = room_status.last_token
|
||||
elif room_status.status == HaveSentRoomFlag.NEVER:
|
||||
initial_rooms.add(room_id)
|
||||
else:
|
||||
assert_never(room_status.status)
|
||||
|
||||
# We fetch all room account data since the from_token. This is so
|
||||
# that we can record which rooms have updates that haven't been sent
|
||||
# down.
|
||||
#
|
||||
# Mapping from room_id to mapping of `type` to `content` of room account
|
||||
# data events.
|
||||
all_updates_since_the_from_token: Mapping[
|
||||
str, Mapping[str, JsonMapping]
|
||||
] = {}
|
||||
if from_token is not None:
|
||||
# TODO: This should take into account the `from_token` and `to_token`
|
||||
account_data_by_room_map = (
|
||||
all_updates_since_the_from_token = (
|
||||
await self.store.get_updated_room_account_data_for_user(
|
||||
user_id, from_token.stream_token.account_data_key
|
||||
)
|
||||
)
|
||||
else:
|
||||
# TODO: This should take into account the `to_token`
|
||||
account_data_by_room_map = (
|
||||
await self.store.get_room_account_data_for_user(user_id)
|
||||
|
||||
# Add room tags
|
||||
#
|
||||
# TODO: This should take into account the `from_token` and `to_token`
|
||||
tags_by_room = await self.store.get_updated_tags(
|
||||
user_id, from_token.stream_token.account_data_key
|
||||
)
|
||||
for room_id, tags in tags_by_room.items():
|
||||
all_updates_since_the_from_token.setdefault(room_id, {})[
|
||||
AccountDataTypes.TAG
|
||||
] = {"tags": tags}
|
||||
|
||||
# For live rooms we just get the updates from `all_updates_since_the_from_token`
|
||||
if live_rooms:
|
||||
for room_id in all_updates_since_the_from_token.keys() & live_rooms:
|
||||
account_data_by_room_map[room_id] = (
|
||||
all_updates_since_the_from_token[room_id]
|
||||
)
|
||||
|
||||
# For previously and initial rooms we query each room individually.
|
||||
if previously_rooms or initial_rooms:
|
||||
|
||||
async def handle_previously(room_id: str) -> None:
|
||||
# Either get updates or all account data in the room
|
||||
# depending on if the room state is PREVIOUSLY or NEVER.
|
||||
previous_token = previously_rooms.get(room_id)
|
||||
if previous_token is not None:
|
||||
room_account_data = await (
|
||||
self.store.get_updated_room_account_data_for_user_for_room(
|
||||
user_id=user_id,
|
||||
room_id=room_id,
|
||||
from_stream_id=previous_token,
|
||||
to_stream_id=to_token.account_data_key,
|
||||
)
|
||||
)
|
||||
|
||||
# Add room tags
|
||||
changed = await self.store.has_tags_changed_for_room(
|
||||
user_id=user_id,
|
||||
room_id=room_id,
|
||||
from_stream_id=previous_token,
|
||||
to_stream_id=to_token.account_data_key,
|
||||
)
|
||||
if changed:
|
||||
# XXX: Ideally, this should take into account the `to_token`
|
||||
# and return the set of tags at that time but we don't track
|
||||
# changes to tags so we just have to return all tags for the
|
||||
# room.
|
||||
immutable_tag_map = await self.store.get_tags_for_room(
|
||||
user_id, room_id
|
||||
)
|
||||
room_account_data[AccountDataTypes.TAG] = {
|
||||
"tags": immutable_tag_map
|
||||
}
|
||||
|
||||
# Only add an entry if there were any updates.
|
||||
if room_account_data:
|
||||
account_data_by_room_map[room_id] = room_account_data
|
||||
else:
|
||||
# TODO: This should take into account the `to_token`
|
||||
immutable_room_account_data = (
|
||||
await self.store.get_account_data_for_room(user_id, room_id)
|
||||
)
|
||||
|
||||
# Add room tags
|
||||
#
|
||||
# XXX: Ideally, this should take into account the `to_token`
|
||||
# and return the set of tags at that time but we don't track
|
||||
# changes to tags so we just have to return all tags for the
|
||||
# room.
|
||||
immutable_tag_map = await self.store.get_tags_for_room(
|
||||
user_id, room_id
|
||||
)
|
||||
|
||||
account_data_by_room_map[room_id] = ChainMap(
|
||||
{AccountDataTypes.TAG: {"tags": immutable_tag_map}}
|
||||
if immutable_tag_map
|
||||
else {},
|
||||
# Cast is safe because `ChainMap` only mutates the top-most map,
|
||||
# see https://github.com/python/typeshed/issues/8430
|
||||
cast(
|
||||
MutableMapping[str, JsonMapping],
|
||||
immutable_room_account_data,
|
||||
),
|
||||
)
|
||||
|
||||
# We handle these rooms concurrently to speed it up.
|
||||
await concurrently_execute(
|
||||
handle_previously,
|
||||
previously_rooms.keys() | initial_rooms,
|
||||
limit=20,
|
||||
)
|
||||
|
||||
# Filter down to the relevant rooms
|
||||
account_data_by_room_map = {
|
||||
room_id: account_data_map
|
||||
for room_id, account_data_map in account_data_by_room_map.items()
|
||||
if room_id in relevant_room_ids
|
||||
}
|
||||
# Now record which rooms are now up to data, and which rooms have
|
||||
# pending updates to send.
|
||||
new_connection_state.account_data.record_sent_rooms(previously_rooms.keys())
|
||||
new_connection_state.account_data.record_sent_rooms(initial_rooms)
|
||||
missing_updates = (
|
||||
all_updates_since_the_from_token.keys() - relevant_room_ids
|
||||
)
|
||||
if missing_updates:
|
||||
# If we have missing updates then we must have had a from_token.
|
||||
assert from_token is not None
|
||||
|
||||
new_connection_state.account_data.record_unsent_rooms(
|
||||
missing_updates, from_token.stream_token.account_data_key
|
||||
)
|
||||
|
||||
return SlidingSyncResult.Extensions.AccountDataExtension(
|
||||
global_account_data_map=global_account_data_map,
|
||||
@@ -534,7 +689,10 @@ class SlidingSyncExtensionHandler:
|
||||
# For rooms we've previously sent down, but aren't up to date, we
|
||||
# need to use the from token from the room status.
|
||||
if previously_rooms:
|
||||
for room_id, receipt_token in previously_rooms.items():
|
||||
# Fetch any missing rooms concurrently.
|
||||
|
||||
async def handle_previously_room(room_id: str) -> None:
|
||||
receipt_token = previously_rooms[room_id]
|
||||
# TODO: Limit the number of receipts we're about to send down
|
||||
# for the room, if its too many we should TODO
|
||||
previously_receipts = (
|
||||
@@ -546,6 +704,10 @@ class SlidingSyncExtensionHandler:
|
||||
)
|
||||
fetched_receipts.extend(previously_receipts)
|
||||
|
||||
await concurrently_execute(
|
||||
handle_previously_room, previously_rooms.keys(), 20
|
||||
)
|
||||
|
||||
if initial_rooms:
|
||||
# We also always send down receipts for the current user.
|
||||
user_receipts = (
|
||||
@@ -602,9 +764,10 @@ class SlidingSyncExtensionHandler:
|
||||
|
||||
room_id_to_receipt_map[room_id] = {"type": type, "content": content}
|
||||
|
||||
# Now we update the per-connection state to track which receipts we have
|
||||
# and haven't sent down.
|
||||
new_connection_state.receipts.record_sent_rooms(relevant_room_ids)
|
||||
# Update the per-connection state to track which rooms we have sent
|
||||
# all the receipts for.
|
||||
new_connection_state.receipts.record_sent_rooms(previously_rooms.keys())
|
||||
new_connection_state.receipts.record_sent_rooms(initial_rooms)
|
||||
|
||||
if from_token:
|
||||
# Now find the set of rooms that may have receipts that we're not sending
|
||||
|
||||
@@ -40,6 +40,7 @@ from synapse.api.constants import (
|
||||
EventTypes,
|
||||
Membership,
|
||||
)
|
||||
from synapse.api.room_versions import KNOWN_ROOM_VERSIONS
|
||||
from synapse.events import StrippedStateEvent
|
||||
from synapse.events.utils import parse_stripped_state_event
|
||||
from synapse.logging.opentracing import start_active_span, trace
|
||||
@@ -55,7 +56,6 @@ from synapse.storage.roommember import (
|
||||
)
|
||||
from synapse.types import (
|
||||
MutableStateMap,
|
||||
PersistedEventPosition,
|
||||
RoomStreamToken,
|
||||
StateMap,
|
||||
StrCollection,
|
||||
@@ -80,6 +80,12 @@ if TYPE_CHECKING:
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class Sentinel(enum.Enum):
|
||||
# defining a sentinel in this way allows mypy to correctly handle the
|
||||
# type of a dictionary lookup and subsequent type narrowing.
|
||||
UNSET_SENTINEL = object()
|
||||
|
||||
|
||||
# Helper definition for the types that we might return. We do this to avoid
|
||||
# copying data between types (which can be expensive for many rooms).
|
||||
RoomsForUserType = Union[RoomsForUserStateReset, RoomsForUser, RoomsForUserSlidingSync]
|
||||
@@ -117,11 +123,18 @@ class SlidingSyncInterestedRooms:
|
||||
newly_left_rooms: AbstractSet[str]
|
||||
dm_room_ids: AbstractSet[str]
|
||||
|
||||
|
||||
class Sentinel(enum.Enum):
|
||||
# defining a sentinel in this way allows mypy to correctly handle the
|
||||
# type of a dictionary lookup and subsequent type narrowing.
|
||||
UNSET_SENTINEL = object()
|
||||
@staticmethod
|
||||
def empty() -> "SlidingSyncInterestedRooms":
|
||||
return SlidingSyncInterestedRooms(
|
||||
lists={},
|
||||
relevant_room_map={},
|
||||
relevant_rooms_to_send_map={},
|
||||
all_rooms=set(),
|
||||
room_membership_for_user_map={},
|
||||
newly_joined_rooms=set(),
|
||||
newly_left_rooms=set(),
|
||||
dm_room_ids=set(),
|
||||
)
|
||||
|
||||
|
||||
def filter_membership_for_sync(
|
||||
@@ -181,6 +194,14 @@ class SlidingSyncRoomLists:
|
||||
from_token: Optional[StreamToken],
|
||||
) -> SlidingSyncInterestedRooms:
|
||||
"""Fetch the set of rooms that match the request"""
|
||||
has_lists = sync_config.lists is not None and len(sync_config.lists) > 0
|
||||
has_room_subscriptions = (
|
||||
sync_config.room_subscriptions is not None
|
||||
and len(sync_config.room_subscriptions) > 0
|
||||
)
|
||||
|
||||
if not has_lists and not has_room_subscriptions:
|
||||
return SlidingSyncInterestedRooms.empty()
|
||||
|
||||
if await self.store.have_finished_sliding_sync_background_jobs():
|
||||
return await self._compute_interested_rooms_new_tables(
|
||||
@@ -220,19 +241,38 @@ class SlidingSyncRoomLists:
|
||||
# include rooms that are outside the list ranges.
|
||||
all_rooms: Set[str] = set()
|
||||
|
||||
# Note: this won't include rooms the user has left themselves. We add back
|
||||
# `newly_left` rooms below. This is more efficient than fetching all rooms and
|
||||
# then filtering out the old left rooms.
|
||||
room_membership_for_user_map = await self.store.get_sliding_sync_rooms_for_user(
|
||||
user_id
|
||||
)
|
||||
|
||||
# Remove invites from ignored users
|
||||
ignored_users = await self.store.ignored_users(user_id)
|
||||
if ignored_users:
|
||||
# TODO: It would be nice to avoid these copies
|
||||
room_membership_for_user_map = dict(room_membership_for_user_map)
|
||||
# Make a copy so we don't run into an error: `dictionary changed size during
|
||||
# iteration`, when we remove items
|
||||
for room_id in list(room_membership_for_user_map.keys()):
|
||||
room_for_user_sliding_sync = room_membership_for_user_map[room_id]
|
||||
if (
|
||||
room_for_user_sliding_sync.membership == Membership.INVITE
|
||||
and room_for_user_sliding_sync.sender in ignored_users
|
||||
):
|
||||
room_membership_for_user_map.pop(room_id, None)
|
||||
|
||||
changes = await self._get_rewind_changes_to_current_membership_to_token(
|
||||
sync_config.user, room_membership_for_user_map, to_token=to_token
|
||||
)
|
||||
if changes:
|
||||
# TODO: It would be nice to avoid these copies
|
||||
room_membership_for_user_map = dict(room_membership_for_user_map)
|
||||
for room_id, change in changes.items():
|
||||
if change is None:
|
||||
# Remove rooms that the user joined after the `to_token`
|
||||
room_membership_for_user_map.pop(room_id)
|
||||
room_membership_for_user_map.pop(room_id, None)
|
||||
continue
|
||||
|
||||
existing_room = room_membership_for_user_map.get(room_id)
|
||||
@@ -245,34 +285,11 @@ class SlidingSyncRoomLists:
|
||||
event_id=change.event_id,
|
||||
event_pos=change.event_pos,
|
||||
room_version_id=change.room_version_id,
|
||||
# We keep the current state of the room though
|
||||
# We keep the state of the room though
|
||||
has_known_state=existing_room.has_known_state,
|
||||
room_type=existing_room.room_type,
|
||||
is_encrypted=existing_room.is_encrypted,
|
||||
)
|
||||
else:
|
||||
# This can happen if we get "state reset" out of the room
|
||||
# after the `to_token`. In other words, there is no membership
|
||||
# for the room after the `to_token` but we see membership in
|
||||
# the token range.
|
||||
|
||||
# Get the state at the time. Note that room type never changes,
|
||||
# so we can just get current room type
|
||||
room_type = await self.store.get_room_type(room_id)
|
||||
is_encrypted = await self.get_is_encrypted_for_room_at_token(
|
||||
room_id, to_token.room_key
|
||||
)
|
||||
|
||||
# Add back rooms that the user was state-reset out of after `to_token`
|
||||
room_membership_for_user_map[room_id] = RoomsForUserSlidingSync(
|
||||
room_id=room_id,
|
||||
sender=change.sender,
|
||||
membership=change.membership,
|
||||
event_id=change.event_id,
|
||||
event_pos=change.event_pos,
|
||||
room_version_id=change.room_version_id,
|
||||
room_type=room_type,
|
||||
is_encrypted=is_encrypted,
|
||||
)
|
||||
|
||||
(
|
||||
newly_joined_room_ids,
|
||||
@@ -282,43 +299,88 @@ class SlidingSyncRoomLists:
|
||||
)
|
||||
dm_room_ids = await self._get_dm_rooms_for_user(user_id)
|
||||
|
||||
# Handle state resets in the from -> to token range.
|
||||
state_reset_rooms = (
|
||||
# Add back `newly_left` rooms (rooms left in the from -> to token range).
|
||||
#
|
||||
# We do this because `get_sliding_sync_rooms_for_user(...)` doesn't include
|
||||
# rooms that the user left themselves as it's more efficient to add them back
|
||||
# here than to fetch all rooms and then filter out the old left rooms. The user
|
||||
# only leaves a room once in a blue moon so this barely needs to run.
|
||||
#
|
||||
missing_newly_left_rooms = (
|
||||
newly_left_room_map.keys() - room_membership_for_user_map.keys()
|
||||
)
|
||||
if state_reset_rooms:
|
||||
if missing_newly_left_rooms:
|
||||
# TODO: It would be nice to avoid these copies
|
||||
room_membership_for_user_map = dict(room_membership_for_user_map)
|
||||
for room_id in (
|
||||
newly_left_room_map.keys() - room_membership_for_user_map.keys()
|
||||
):
|
||||
# Get the state at the time. Note that room type never changes,
|
||||
# so we can just get current room type
|
||||
room_type = await self.store.get_room_type(room_id)
|
||||
is_encrypted = await self.get_is_encrypted_for_room_at_token(
|
||||
room_id, newly_left_room_map[room_id].to_room_stream_token()
|
||||
)
|
||||
for room_id in missing_newly_left_rooms:
|
||||
newly_left_room_for_user = newly_left_room_map[room_id]
|
||||
# This should be a given
|
||||
assert newly_left_room_for_user.membership == Membership.LEAVE
|
||||
|
||||
room_membership_for_user_map[room_id] = RoomsForUserSlidingSync(
|
||||
room_id=room_id,
|
||||
sender=None,
|
||||
membership=Membership.LEAVE,
|
||||
event_id=None,
|
||||
event_pos=newly_left_room_map[room_id],
|
||||
room_version_id=await self.store.get_room_version_id(room_id),
|
||||
room_type=room_type,
|
||||
is_encrypted=is_encrypted,
|
||||
# Add back `newly_left` rooms
|
||||
#
|
||||
# Check for membership and state in the Sliding Sync tables as it's just
|
||||
# another membership
|
||||
newly_left_room_for_user_sliding_sync = (
|
||||
await self.store.get_sliding_sync_room_for_user(user_id, room_id)
|
||||
)
|
||||
# If the membership exists, it's just a normal user left the room on
|
||||
# their own
|
||||
if newly_left_room_for_user_sliding_sync is not None:
|
||||
room_membership_for_user_map[room_id] = (
|
||||
newly_left_room_for_user_sliding_sync
|
||||
)
|
||||
|
||||
change = changes.get(room_id)
|
||||
if change is not None:
|
||||
# Update room membership events to the point in time of the `to_token`
|
||||
room_membership_for_user_map[room_id] = RoomsForUserSlidingSync(
|
||||
room_id=room_id,
|
||||
sender=change.sender,
|
||||
membership=change.membership,
|
||||
event_id=change.event_id,
|
||||
event_pos=change.event_pos,
|
||||
room_version_id=change.room_version_id,
|
||||
# We keep the state of the room though
|
||||
has_known_state=newly_left_room_for_user_sliding_sync.has_known_state,
|
||||
room_type=newly_left_room_for_user_sliding_sync.room_type,
|
||||
is_encrypted=newly_left_room_for_user_sliding_sync.is_encrypted,
|
||||
)
|
||||
|
||||
# If we are `newly_left` from the room but can't find any membership,
|
||||
# then we have been "state reset" out of the room
|
||||
else:
|
||||
# Get the state at the time. We can't read from the Sliding Sync
|
||||
# tables because the user has no membership in the room according to
|
||||
# the state (thanks to the state reset).
|
||||
#
|
||||
# Note: `room_type` never changes, so we can just get current room
|
||||
# type
|
||||
room_type = await self.store.get_room_type(room_id)
|
||||
has_known_state = room_type is not ROOM_UNKNOWN_SENTINEL
|
||||
if isinstance(room_type, StateSentinel):
|
||||
room_type = None
|
||||
|
||||
# Get the encryption status at the time of the token
|
||||
is_encrypted = await self.get_is_encrypted_for_room_at_token(
|
||||
room_id,
|
||||
newly_left_room_for_user.event_pos.to_room_stream_token(),
|
||||
)
|
||||
|
||||
room_membership_for_user_map[room_id] = RoomsForUserSlidingSync(
|
||||
room_id=room_id,
|
||||
sender=newly_left_room_for_user.sender,
|
||||
membership=newly_left_room_for_user.membership,
|
||||
event_id=newly_left_room_for_user.event_id,
|
||||
event_pos=newly_left_room_for_user.event_pos,
|
||||
room_version_id=newly_left_room_for_user.room_version_id,
|
||||
has_known_state=has_known_state,
|
||||
room_type=room_type,
|
||||
is_encrypted=is_encrypted,
|
||||
)
|
||||
|
||||
if sync_config.lists:
|
||||
sync_room_map = {
|
||||
room_id: room_membership_for_user
|
||||
for room_id, room_membership_for_user in room_membership_for_user_map.items()
|
||||
if filter_membership_for_sync(
|
||||
user_id=user_id,
|
||||
room_membership_for_user=room_membership_for_user,
|
||||
newly_left=room_id in newly_left_room_map,
|
||||
)
|
||||
}
|
||||
sync_room_map = room_membership_for_user_map
|
||||
with start_active_span("assemble_sliding_window_lists"):
|
||||
for list_key, list_config in sync_config.lists.items():
|
||||
# Apply filters
|
||||
@@ -327,6 +389,7 @@ class SlidingSyncRoomLists:
|
||||
filtered_sync_room_map = await self.filter_rooms_using_tables(
|
||||
user_id,
|
||||
sync_room_map,
|
||||
previous_connection_state,
|
||||
list_config.filters,
|
||||
to_token,
|
||||
dm_room_ids,
|
||||
@@ -354,8 +417,18 @@ class SlidingSyncRoomLists:
|
||||
ops: List[SlidingSyncResult.SlidingWindowList.Operation] = []
|
||||
|
||||
if list_config.ranges:
|
||||
if list_config.ranges == [(0, len(filtered_sync_room_map) - 1)]:
|
||||
# If we are asking for the full range, we don't need to sort the list.
|
||||
# Optimization: If we are asking for the full range, we don't
|
||||
# need to sort the list.
|
||||
if (
|
||||
# We're looking for a single range that covers the entire list
|
||||
len(list_config.ranges) == 1
|
||||
# Range starts at 0
|
||||
and list_config.ranges[0][0] == 0
|
||||
# And the range extends to the end of the list or more. Each
|
||||
# side is inclusive.
|
||||
and list_config.ranges[0][1]
|
||||
>= len(filtered_sync_room_map) - 1
|
||||
):
|
||||
sorted_room_info: List[RoomsForUserType] = list(
|
||||
filtered_sync_room_map.values()
|
||||
)
|
||||
@@ -368,6 +441,10 @@ class SlidingSyncRoomLists:
|
||||
Dict[str, RoomsForUserType], filtered_sync_room_map
|
||||
),
|
||||
to_token,
|
||||
# We only need to sort the rooms up to the end
|
||||
# of the largest range. Both sides of range are
|
||||
# inclusive so we `+ 1`.
|
||||
limit=max(range[1] + 1 for range in list_config.ranges),
|
||||
)
|
||||
|
||||
for range in list_config.ranges:
|
||||
@@ -410,21 +487,38 @@ class SlidingSyncRoomLists:
|
||||
)
|
||||
|
||||
lists[list_key] = SlidingSyncResult.SlidingWindowList(
|
||||
count=len(sorted_room_info),
|
||||
count=len(filtered_sync_room_map),
|
||||
ops=ops,
|
||||
)
|
||||
|
||||
if sync_config.room_subscriptions:
|
||||
with start_active_span("assemble_room_subscriptions"):
|
||||
# TODO: It would be nice to avoid these copies
|
||||
room_membership_for_user_map = dict(room_membership_for_user_map)
|
||||
|
||||
# Find which rooms are partially stated and may need to be filtered out
|
||||
# depending on the `required_state` requested (see below).
|
||||
partial_state_rooms = await self.store.get_partial_rooms()
|
||||
|
||||
# Fetch any rooms that we have not already fetched from the database.
|
||||
subscription_sliding_sync_rooms = (
|
||||
await self.store.get_sliding_sync_room_for_user_batch(
|
||||
user_id,
|
||||
sync_config.room_subscriptions.keys()
|
||||
- room_membership_for_user_map.keys(),
|
||||
)
|
||||
)
|
||||
room_membership_for_user_map.update(subscription_sliding_sync_rooms)
|
||||
|
||||
for (
|
||||
room_id,
|
||||
room_subscription,
|
||||
) in sync_config.room_subscriptions.items():
|
||||
if room_id not in room_membership_for_user_map:
|
||||
# Check if we have a membership for the room, but didn't pull it out
|
||||
# above. This could be e.g. a leave that we don't pull out by
|
||||
# default.
|
||||
current_room_entry = room_membership_for_user_map.get(room_id)
|
||||
if not current_room_entry:
|
||||
# TODO: Handle rooms the user isn't in.
|
||||
continue
|
||||
|
||||
@@ -441,8 +535,6 @@ class SlidingSyncRoomLists:
|
||||
if room_id in partial_state_rooms:
|
||||
continue
|
||||
|
||||
all_rooms.add(room_id)
|
||||
|
||||
# Update our `relevant_room_map` with the room we're going to display
|
||||
# and need to fetch more info about.
|
||||
existing_room_sync_config = relevant_room_map.get(room_id)
|
||||
@@ -457,7 +549,7 @@ class SlidingSyncRoomLists:
|
||||
|
||||
# Filtered subset of `relevant_room_map` for rooms that may have updates
|
||||
# (in the event stream)
|
||||
relevant_rooms_to_send_map = await self._filter_relevant_room_to_send(
|
||||
relevant_rooms_to_send_map = await self._filter_relevant_rooms_to_send(
|
||||
previous_connection_state, from_token, relevant_room_map
|
||||
)
|
||||
|
||||
@@ -514,6 +606,7 @@ class SlidingSyncRoomLists:
|
||||
filtered_sync_room_map = await self.filter_rooms(
|
||||
sync_config.user,
|
||||
sync_room_map,
|
||||
previous_connection_state,
|
||||
list_config.filters,
|
||||
to_token,
|
||||
dm_room_ids,
|
||||
@@ -644,7 +737,7 @@ class SlidingSyncRoomLists:
|
||||
|
||||
# Filtered subset of `relevant_room_map` for rooms that may have updates
|
||||
# (in the event stream)
|
||||
relevant_rooms_to_send_map = await self._filter_relevant_room_to_send(
|
||||
relevant_rooms_to_send_map = await self._filter_relevant_rooms_to_send(
|
||||
previous_connection_state, from_token, relevant_room_map
|
||||
)
|
||||
|
||||
@@ -659,7 +752,7 @@ class SlidingSyncRoomLists:
|
||||
dm_room_ids=dm_room_ids,
|
||||
)
|
||||
|
||||
async def _filter_relevant_room_to_send(
|
||||
async def _filter_relevant_rooms_to_send(
|
||||
self,
|
||||
previous_connection_state: PerConnectionState,
|
||||
from_token: Optional[StreamToken],
|
||||
@@ -923,8 +1016,38 @@ class SlidingSyncRoomLists:
|
||||
excluded_rooms=self.rooms_to_exclude_globally,
|
||||
)
|
||||
|
||||
# If the user has never joined any rooms before, we can just return an empty list
|
||||
if not room_for_user_list:
|
||||
# We filter out unknown room versions before we try and load any
|
||||
# metadata about the room. They shouldn't go down sync anyway, and their
|
||||
# metadata may be in a broken state.
|
||||
room_for_user_list = [
|
||||
room_for_user
|
||||
for room_for_user in room_for_user_list
|
||||
if room_for_user.room_version_id in KNOWN_ROOM_VERSIONS
|
||||
]
|
||||
|
||||
# Remove invites from ignored users
|
||||
ignored_users = await self.store.ignored_users(user_id)
|
||||
if ignored_users:
|
||||
room_for_user_list = [
|
||||
room_for_user
|
||||
for room_for_user in room_for_user_list
|
||||
if not (
|
||||
room_for_user.membership == Membership.INVITE
|
||||
and room_for_user.sender in ignored_users
|
||||
)
|
||||
]
|
||||
|
||||
(
|
||||
newly_joined_room_ids,
|
||||
newly_left_room_map,
|
||||
) = await self._get_newly_joined_and_left_rooms(
|
||||
user_id, to_token=to_token, from_token=from_token
|
||||
)
|
||||
|
||||
# If the user has never joined any rooms before, we can just return an empty
|
||||
# list. We also have to check the `newly_left_room_map` in case someone was
|
||||
# state reset out of all of the rooms they were in.
|
||||
if not room_for_user_list and not newly_left_room_map:
|
||||
return {}, set(), set()
|
||||
|
||||
# Since we fetched the users room list at some point in time after the
|
||||
@@ -942,30 +1065,22 @@ class SlidingSyncRoomLists:
|
||||
else:
|
||||
rooms_for_user[room_id] = change_room_for_user
|
||||
|
||||
(
|
||||
newly_joined_room_ids,
|
||||
newly_left_room_ids,
|
||||
) = await self._get_newly_joined_and_left_rooms(
|
||||
user_id, to_token=to_token, from_token=from_token
|
||||
)
|
||||
|
||||
# Ensure we have entries for rooms that the user has been "state reset"
|
||||
# out of. These are rooms appear in the `newly_left_rooms` map but
|
||||
# aren't in the `rooms_for_user` map.
|
||||
for room_id, left_event_pos in newly_left_room_ids.items():
|
||||
for room_id, newly_left_room_for_user in newly_left_room_map.items():
|
||||
# If we already know about the room, it's not a state reset
|
||||
if room_id in rooms_for_user:
|
||||
continue
|
||||
|
||||
rooms_for_user[room_id] = RoomsForUserStateReset(
|
||||
room_id=room_id,
|
||||
event_id=None,
|
||||
event_pos=left_event_pos,
|
||||
membership=Membership.LEAVE,
|
||||
sender=None,
|
||||
room_version_id=await self.store.get_room_version_id(room_id),
|
||||
)
|
||||
# This should be true if it's a state reset
|
||||
assert newly_left_room_for_user.membership is Membership.LEAVE
|
||||
assert newly_left_room_for_user.event_id is None
|
||||
assert newly_left_room_for_user.sender is None
|
||||
|
||||
return rooms_for_user, newly_joined_room_ids, set(newly_left_room_ids)
|
||||
rooms_for_user[room_id] = newly_left_room_for_user
|
||||
|
||||
return rooms_for_user, newly_joined_room_ids, set(newly_left_room_map)
|
||||
|
||||
@trace
|
||||
async def _get_newly_joined_and_left_rooms(
|
||||
@@ -973,7 +1088,7 @@ class SlidingSyncRoomLists:
|
||||
user_id: str,
|
||||
to_token: StreamToken,
|
||||
from_token: Optional[StreamToken],
|
||||
) -> Tuple[AbstractSet[str], Mapping[str, PersistedEventPosition]]:
|
||||
) -> Tuple[AbstractSet[str], Mapping[str, RoomsForUserStateReset]]:
|
||||
"""Fetch the sets of rooms that the user newly joined or left in the
|
||||
given token range.
|
||||
|
||||
@@ -982,11 +1097,18 @@ class SlidingSyncRoomLists:
|
||||
"current memberships" of the user.
|
||||
|
||||
Returns:
|
||||
A 2-tuple of newly joined room IDs and a map of newly left room
|
||||
IDs to the event position the leave happened at.
|
||||
A 2-tuple of newly joined room IDs and a map of newly_left room
|
||||
IDs to the `RoomsForUserStateReset` entry.
|
||||
|
||||
We're using `RoomsForUserStateReset` but that doesn't necessarily mean the
|
||||
user was state reset of the rooms. It's just that the `event_id`/`sender`
|
||||
are optional and we can't tell the difference between the server leaving the
|
||||
room when the user was the last person participating in the room and left or
|
||||
was state reset out of the room. To actually check for a state reset, you
|
||||
need to check if a membership still exists in the room.
|
||||
"""
|
||||
newly_joined_room_ids: Set[str] = set()
|
||||
newly_left_room_map: Dict[str, PersistedEventPosition] = {}
|
||||
newly_left_room_map: Dict[str, RoomsForUserStateReset] = {}
|
||||
|
||||
# We need to figure out the
|
||||
#
|
||||
@@ -1057,8 +1179,13 @@ class SlidingSyncRoomLists:
|
||||
# 1) Figure out newly_left rooms (> `from_token` and <= `to_token`).
|
||||
if last_membership_change_in_from_to_range.membership == Membership.LEAVE:
|
||||
# 1) Mark this room as `newly_left`
|
||||
newly_left_room_map[room_id] = (
|
||||
last_membership_change_in_from_to_range.event_pos
|
||||
newly_left_room_map[room_id] = RoomsForUserStateReset(
|
||||
room_id=room_id,
|
||||
sender=last_membership_change_in_from_to_range.sender,
|
||||
membership=Membership.LEAVE,
|
||||
event_id=last_membership_change_in_from_to_range.event_id,
|
||||
event_pos=last_membership_change_in_from_to_range.event_pos,
|
||||
room_version_id=await self.store.get_room_version_id(room_id),
|
||||
)
|
||||
|
||||
# 2) Figure out `newly_joined`
|
||||
@@ -1502,6 +1629,7 @@ class SlidingSyncRoomLists:
|
||||
self,
|
||||
user: UserID,
|
||||
sync_room_map: Dict[str, RoomsForUserType],
|
||||
previous_connection_state: PerConnectionState,
|
||||
filters: SlidingSyncConfig.SlidingSyncList.Filters,
|
||||
to_token: StreamToken,
|
||||
dm_room_ids: AbstractSet[str],
|
||||
@@ -1521,6 +1649,8 @@ class SlidingSyncRoomLists:
|
||||
A filtered dictionary of room IDs along with membership information in the
|
||||
room at the time of `to_token`.
|
||||
"""
|
||||
user_id = user.to_string()
|
||||
|
||||
room_id_to_stripped_state_map: Dict[
|
||||
str, Optional[StateMap[StrippedStateEvent]]
|
||||
] = {}
|
||||
@@ -1630,12 +1760,14 @@ class SlidingSyncRoomLists:
|
||||
and room_type not in filters.room_types
|
||||
):
|
||||
filtered_room_id_set.remove(room_id)
|
||||
continue
|
||||
|
||||
if (
|
||||
filters.not_room_types is not None
|
||||
and room_type in filters.not_room_types
|
||||
):
|
||||
filtered_room_id_set.remove(room_id)
|
||||
continue
|
||||
|
||||
if filters.room_name_like is not None:
|
||||
with start_active_span("filters.room_name_like"):
|
||||
@@ -1652,18 +1784,64 @@ class SlidingSyncRoomLists:
|
||||
# )
|
||||
raise NotImplementedError()
|
||||
|
||||
# Filter by room tags according to the users account data
|
||||
if filters.tags is not None or filters.not_tags is not None:
|
||||
with start_active_span("filters.tags"):
|
||||
raise NotImplementedError()
|
||||
# Fetch the user tags for their rooms
|
||||
room_tags = await self.store.get_tags_for_user(user_id)
|
||||
room_id_to_tag_name_set: Dict[str, Set[str]] = {
|
||||
room_id: set(tags.keys()) for room_id, tags in room_tags.items()
|
||||
}
|
||||
|
||||
if filters.tags is not None:
|
||||
tags_set = set(filters.tags)
|
||||
filtered_room_id_set = {
|
||||
room_id
|
||||
for room_id in filtered_room_id_set
|
||||
# Remove rooms that don't have one of the tags in the filter
|
||||
if room_id_to_tag_name_set.get(room_id, set()).intersection(
|
||||
tags_set
|
||||
)
|
||||
}
|
||||
|
||||
if filters.not_tags is not None:
|
||||
not_tags_set = set(filters.not_tags)
|
||||
filtered_room_id_set = {
|
||||
room_id
|
||||
for room_id in filtered_room_id_set
|
||||
# Remove rooms if they have any of the tags in the filter
|
||||
if not room_id_to_tag_name_set.get(room_id, set()).intersection(
|
||||
not_tags_set
|
||||
)
|
||||
}
|
||||
|
||||
# Keep rooms if the user has been state reset out of it but we previously sent
|
||||
# down the connection before. We want to make sure that we send these down to
|
||||
# the client regardless of filters so they find out about the state reset.
|
||||
#
|
||||
# We don't always have access to the state in a room after being state reset if
|
||||
# no one else locally on the server is participating in the room so we patch
|
||||
# these back in manually.
|
||||
state_reset_out_of_room_id_set = {
|
||||
room_id
|
||||
for room_id in sync_room_map.keys()
|
||||
if sync_room_map[room_id].event_id is None
|
||||
and previous_connection_state.rooms.have_sent_room(room_id).status
|
||||
!= HaveSentRoomFlag.NEVER
|
||||
}
|
||||
|
||||
# Assemble a new sync room map but only with the `filtered_room_id_set`
|
||||
return {room_id: sync_room_map[room_id] for room_id in filtered_room_id_set}
|
||||
return {
|
||||
room_id: sync_room_map[room_id]
|
||||
for room_id in filtered_room_id_set | state_reset_out_of_room_id_set
|
||||
}
|
||||
|
||||
@trace
|
||||
async def filter_rooms_using_tables(
|
||||
self,
|
||||
user_id: str,
|
||||
sync_room_map: Mapping[str, RoomsForUserSlidingSync],
|
||||
previous_connection_state: PerConnectionState,
|
||||
filters: SlidingSyncConfig.SlidingSyncList.Filters,
|
||||
to_token: StreamToken,
|
||||
dm_room_ids: AbstractSet[str],
|
||||
@@ -1678,6 +1856,7 @@ class SlidingSyncRoomLists:
|
||||
filters: Filters to apply
|
||||
to_token: We filter based on the state of the room at this token
|
||||
dm_room_ids: Set of room IDs which are DMs
|
||||
room_tags: Mapping of room ID to tags
|
||||
|
||||
Returns:
|
||||
A filtered dictionary of room IDs along with membership information in the
|
||||
@@ -1705,7 +1884,10 @@ class SlidingSyncRoomLists:
|
||||
filtered_room_id_set = {
|
||||
room_id
|
||||
for room_id in filtered_room_id_set
|
||||
if sync_room_map[room_id].is_encrypted == filters.is_encrypted
|
||||
# Remove rooms if we can't figure out what the encryption status is
|
||||
if sync_room_map[room_id].has_known_state
|
||||
# Or remove if it doesn't match the filter
|
||||
and sync_room_map[room_id].is_encrypted == filters.is_encrypted
|
||||
}
|
||||
|
||||
# Filter for rooms that the user has been invited to
|
||||
@@ -1734,6 +1916,11 @@ class SlidingSyncRoomLists:
|
||||
# Make a copy so we don't run into an error: `Set changed size during
|
||||
# iteration`, when we filter out and remove items
|
||||
for room_id in filtered_room_id_set.copy():
|
||||
# Remove rooms if we can't figure out what room type it is
|
||||
if not sync_room_map[room_id].has_known_state:
|
||||
filtered_room_id_set.remove(room_id)
|
||||
continue
|
||||
|
||||
room_type = sync_room_map[room_id].room_type
|
||||
|
||||
if (
|
||||
@@ -1741,12 +1928,14 @@ class SlidingSyncRoomLists:
|
||||
and room_type not in filters.room_types
|
||||
):
|
||||
filtered_room_id_set.remove(room_id)
|
||||
continue
|
||||
|
||||
if (
|
||||
filters.not_room_types is not None
|
||||
and room_type in filters.not_room_types
|
||||
):
|
||||
filtered_room_id_set.remove(room_id)
|
||||
continue
|
||||
|
||||
if filters.room_name_like is not None:
|
||||
with start_active_span("filters.room_name_like"):
|
||||
@@ -1763,27 +1952,78 @@ class SlidingSyncRoomLists:
|
||||
# )
|
||||
raise NotImplementedError()
|
||||
|
||||
# Filter by room tags according to the users account data
|
||||
if filters.tags is not None or filters.not_tags is not None:
|
||||
with start_active_span("filters.tags"):
|
||||
raise NotImplementedError()
|
||||
# Fetch the user tags for their rooms
|
||||
room_tags = await self.store.get_tags_for_user(user_id)
|
||||
room_id_to_tag_name_set: Dict[str, Set[str]] = {
|
||||
room_id: set(tags.keys()) for room_id, tags in room_tags.items()
|
||||
}
|
||||
|
||||
if filters.tags is not None:
|
||||
tags_set = set(filters.tags)
|
||||
filtered_room_id_set = {
|
||||
room_id
|
||||
for room_id in filtered_room_id_set
|
||||
# Remove rooms that don't have one of the tags in the filter
|
||||
if room_id_to_tag_name_set.get(room_id, set()).intersection(
|
||||
tags_set
|
||||
)
|
||||
}
|
||||
|
||||
if filters.not_tags is not None:
|
||||
not_tags_set = set(filters.not_tags)
|
||||
filtered_room_id_set = {
|
||||
room_id
|
||||
for room_id in filtered_room_id_set
|
||||
# Remove rooms if they have any of the tags in the filter
|
||||
if not room_id_to_tag_name_set.get(room_id, set()).intersection(
|
||||
not_tags_set
|
||||
)
|
||||
}
|
||||
|
||||
# Keep rooms if the user has been state reset out of it but we previously sent
|
||||
# down the connection before. We want to make sure that we send these down to
|
||||
# the client regardless of filters so they find out about the state reset.
|
||||
#
|
||||
# We don't always have access to the state in a room after being state reset if
|
||||
# no one else locally on the server is participating in the room so we patch
|
||||
# these back in manually.
|
||||
state_reset_out_of_room_id_set = {
|
||||
room_id
|
||||
for room_id in sync_room_map.keys()
|
||||
if sync_room_map[room_id].event_id is None
|
||||
and previous_connection_state.rooms.have_sent_room(room_id).status
|
||||
!= HaveSentRoomFlag.NEVER
|
||||
}
|
||||
|
||||
# Assemble a new sync room map but only with the `filtered_room_id_set`
|
||||
return {room_id: sync_room_map[room_id] for room_id in filtered_room_id_set}
|
||||
return {
|
||||
room_id: sync_room_map[room_id]
|
||||
for room_id in filtered_room_id_set | state_reset_out_of_room_id_set
|
||||
}
|
||||
|
||||
@trace
|
||||
async def sort_rooms(
|
||||
self,
|
||||
sync_room_map: Dict[str, RoomsForUserType],
|
||||
to_token: StreamToken,
|
||||
limit: Optional[int] = None,
|
||||
) -> List[RoomsForUserType]:
|
||||
"""
|
||||
Sort by `stream_ordering` of the last event that the user should see in the
|
||||
room. `stream_ordering` is unique so we get a stable sort.
|
||||
|
||||
If `limit` is specified then sort may return fewer entries, but will
|
||||
always return at least the top N rooms. This is useful as we don't always
|
||||
need to sort the full list, but are just interested in the top N.
|
||||
|
||||
Args:
|
||||
sync_room_map: Dictionary of room IDs to sort along with membership
|
||||
information in the room at the time of `to_token`.
|
||||
to_token: We sort based on the events in the room at this token (<= `to_token`)
|
||||
limit: The number of rooms that we need to return from the top of the list.
|
||||
|
||||
Returns:
|
||||
A sorted list of room IDs by `stream_ordering` along with membership information.
|
||||
@@ -1793,8 +2033,23 @@ class SlidingSyncRoomLists:
|
||||
# user should see in the room (<= `to_token`)
|
||||
last_activity_in_room_map: Dict[str, int] = {}
|
||||
|
||||
# Same as above, except for positions that we know are in the event
|
||||
# stream cache.
|
||||
cached_positions: Dict[str, int] = {}
|
||||
|
||||
earliest_cache_position = (
|
||||
self.store._events_stream_cache.get_earliest_known_position()
|
||||
)
|
||||
|
||||
for room_id, room_for_user in sync_room_map.items():
|
||||
if room_for_user.membership != Membership.JOIN:
|
||||
if room_for_user.membership == Membership.JOIN:
|
||||
# For joined rooms check the stream change cache.
|
||||
cached_position = (
|
||||
self.store._events_stream_cache.get_max_pos_of_last_change(room_id)
|
||||
)
|
||||
if cached_position is not None:
|
||||
cached_positions[room_id] = cached_position
|
||||
else:
|
||||
# If the user has left/been invited/knocked/been banned from a
|
||||
# room, they shouldn't see anything past that point.
|
||||
#
|
||||
@@ -1804,6 +2059,48 @@ class SlidingSyncRoomLists:
|
||||
# https://github.com/matrix-org/matrix-spec-proposals/pull/3575#discussion_r1653045932
|
||||
last_activity_in_room_map[room_id] = room_for_user.event_pos.stream
|
||||
|
||||
# If the stream position is in range of the stream change cache
|
||||
# we can include it.
|
||||
if room_for_user.event_pos.stream > earliest_cache_position:
|
||||
cached_positions[room_id] = room_for_user.event_pos.stream
|
||||
|
||||
# If we are only asked for the top N rooms, and we have enough from
|
||||
# looking in the stream change cache, then we can return early. This
|
||||
# is because the cache must include all entries above
|
||||
# `.get_earliest_known_position()`.
|
||||
if limit is not None and len(cached_positions) >= limit:
|
||||
# ... but first we need to handle the case where the cached max
|
||||
# position is greater than the to_token, in which case we do
|
||||
# actually query the DB. This should happen rarely, so can do it in
|
||||
# a loop.
|
||||
for room_id, position in list(cached_positions.items()):
|
||||
if position > to_token.room_key.stream:
|
||||
result = await self.store.get_last_event_pos_in_room_before_stream_ordering(
|
||||
room_id, to_token.room_key
|
||||
)
|
||||
if (
|
||||
result is not None
|
||||
and result[1].stream > earliest_cache_position
|
||||
):
|
||||
# We have a stream position in the cached range.
|
||||
cached_positions[room_id] = result[1].stream
|
||||
else:
|
||||
# No position in the range, so we remove the entry.
|
||||
cached_positions.pop(room_id)
|
||||
|
||||
if limit is not None and len(cached_positions) >= limit:
|
||||
return sorted(
|
||||
(
|
||||
room
|
||||
for room in sync_room_map.values()
|
||||
if room.room_id in cached_positions
|
||||
),
|
||||
# Sort by the last activity (stream_ordering) in the room
|
||||
key=lambda room_info: cached_positions[room_info.room_id],
|
||||
# We want descending order
|
||||
reverse=True,
|
||||
)
|
||||
|
||||
# For fully-joined rooms, we find the latest activity at/before the
|
||||
# `to_token`.
|
||||
joined_room_positions = (
|
||||
|
||||
@@ -1397,6 +1397,7 @@ class SyncHandler:
|
||||
timeline_contains=timeline_state,
|
||||
timeline_start=state_at_timeline_start,
|
||||
timeline_end=state_at_timeline_end,
|
||||
previous_timeline_start={},
|
||||
previous_timeline_end={},
|
||||
lazy_load_members=lazy_load_members,
|
||||
)
|
||||
@@ -1535,6 +1536,17 @@ class SyncHandler:
|
||||
await_full_state=await_full_state,
|
||||
)
|
||||
|
||||
state_at_previous_sync_start = (
|
||||
{}
|
||||
if since_token.prev_batch is None
|
||||
else await self._state_storage_controller.get_state_ids_at(
|
||||
room_id,
|
||||
stream_position=since_token.prev_batch,
|
||||
state_filter=state_filter,
|
||||
await_full_state=await_full_state,
|
||||
)
|
||||
)
|
||||
|
||||
state_at_timeline_end = await self._state_storage_controller.get_state_ids_at(
|
||||
room_id,
|
||||
stream_position=end_token,
|
||||
@@ -1546,6 +1558,7 @@ class SyncHandler:
|
||||
timeline_contains=timeline_state,
|
||||
timeline_start=state_at_timeline_start,
|
||||
timeline_end=state_at_timeline_end,
|
||||
previous_timeline_start=state_at_previous_sync_start,
|
||||
previous_timeline_end=state_at_previous_sync,
|
||||
lazy_load_members=lazy_load_members,
|
||||
)
|
||||
@@ -1965,7 +1978,7 @@ class SyncHandler:
|
||||
# this is due to some of the underlying streams not supporting the ability
|
||||
# to query up to a given point.
|
||||
# Always use the `now_token` in `SyncResultBuilder`
|
||||
now_token = self.event_sources.get_current_token()
|
||||
now_token = self.event_sources.get_current_token(prev_batch=since_token)
|
||||
log_kv({"now_token": now_token})
|
||||
|
||||
# Since we fetched the users room list before calculating the `now_token` (see
|
||||
@@ -2980,6 +2993,7 @@ def _calculate_state(
|
||||
timeline_contains: StateMap[str],
|
||||
timeline_start: StateMap[str],
|
||||
timeline_end: StateMap[str],
|
||||
previous_timeline_start: StateMap[str],
|
||||
previous_timeline_end: StateMap[str],
|
||||
lazy_load_members: bool,
|
||||
) -> StateMap[str]:
|
||||
@@ -3007,6 +3021,7 @@ def _calculate_state(
|
||||
|
||||
timeline_end_ids = set(timeline_end.values())
|
||||
timeline_start_ids = set(timeline_start.values())
|
||||
previous_timeline_start_ids = set(previous_timeline_start.values())
|
||||
previous_timeline_end_ids = set(previous_timeline_end.values())
|
||||
timeline_contains_ids = set(timeline_contains.values())
|
||||
|
||||
@@ -3082,7 +3097,7 @@ def _calculate_state(
|
||||
|
||||
state_ids = (
|
||||
(timeline_end_ids | timeline_start_ids)
|
||||
- previous_timeline_end_ids
|
||||
- (previous_timeline_end_ids | previous_timeline_start_ids)
|
||||
- timeline_contains_ids
|
||||
)
|
||||
|
||||
|
||||
@@ -1039,7 +1039,7 @@ class _MultipartParserProtocol(protocol.Protocol):
|
||||
self.deferred = deferred
|
||||
self.boundary = boundary
|
||||
self.max_length = max_length
|
||||
self.parser = None
|
||||
self.parser: Optional[multipart.MultipartParser] = None
|
||||
self.multipart_response = MultipartResponse()
|
||||
self.has_redirect = False
|
||||
self.in_json = False
|
||||
@@ -1097,7 +1097,7 @@ class _MultipartParserProtocol(protocol.Protocol):
|
||||
self.deferred.errback()
|
||||
self.file_length += end - start
|
||||
|
||||
callbacks = {
|
||||
callbacks: "multipart.multipart.MultipartCallbacks" = {
|
||||
"on_header_field": on_header_field,
|
||||
"on_header_value": on_header_value,
|
||||
"on_part_data": on_part_data,
|
||||
@@ -1113,7 +1113,7 @@ class _MultipartParserProtocol(protocol.Protocol):
|
||||
self.transport.abortConnection()
|
||||
|
||||
try:
|
||||
self.parser.write(incoming_data) # type: ignore[attr-defined]
|
||||
self.parser.write(incoming_data)
|
||||
except Exception as e:
|
||||
logger.warning(f"Exception writing to multipart parser: {e}")
|
||||
self.deferred.errback()
|
||||
|
||||
@@ -19,7 +19,6 @@
|
||||
#
|
||||
#
|
||||
import abc
|
||||
import cgi
|
||||
import codecs
|
||||
import logging
|
||||
import random
|
||||
@@ -792,7 +791,7 @@ class MatrixFederationHttpClient:
|
||||
url_str,
|
||||
_flatten_response_never_received(e),
|
||||
)
|
||||
body = None
|
||||
body = b""
|
||||
|
||||
exc = HttpResponseException(
|
||||
response.code, response_phrase, body
|
||||
@@ -1813,8 +1812,9 @@ def check_content_type_is(headers: Headers, expected_content_type: str) -> None:
|
||||
)
|
||||
|
||||
c_type = content_type_headers[0].decode("ascii") # only the first header
|
||||
val, options = cgi.parse_header(c_type)
|
||||
if val != expected_content_type:
|
||||
# Extract the 'essence' of the mimetype, removing any parameter
|
||||
c_type_parsed = c_type.split(";", 1)[0].strip()
|
||||
if c_type_parsed != expected_content_type:
|
||||
raise RequestSendFailed(
|
||||
RuntimeError(
|
||||
f"Remote server sent Content-Type header of '{c_type}', not '{expected_content_type}'",
|
||||
|
||||
@@ -37,19 +37,17 @@ from typing import (
|
||||
overload,
|
||||
)
|
||||
|
||||
from synapse._pydantic_compat import HAS_PYDANTIC_V2
|
||||
|
||||
if TYPE_CHECKING or HAS_PYDANTIC_V2:
|
||||
from pydantic.v1 import BaseModel, MissingError, PydanticValueError, ValidationError
|
||||
from pydantic.v1.error_wrappers import ErrorWrapper
|
||||
else:
|
||||
from pydantic import BaseModel, MissingError, PydanticValueError, ValidationError
|
||||
from pydantic.error_wrappers import ErrorWrapper
|
||||
|
||||
from typing_extensions import Literal
|
||||
|
||||
from twisted.web.server import Request
|
||||
|
||||
from synapse._pydantic_compat import (
|
||||
BaseModel,
|
||||
ErrorWrapper,
|
||||
MissingError,
|
||||
PydanticValueError,
|
||||
ValidationError,
|
||||
)
|
||||
from synapse.api.errors import Codes, SynapseError
|
||||
from synapse.http import redact_uri
|
||||
from synapse.http.server import HttpServer
|
||||
|
||||
@@ -206,7 +206,7 @@ class Thumbnailer:
|
||||
def _encode_image(self, output_image: Image.Image, output_type: str) -> BytesIO:
|
||||
output_bytes_io = BytesIO()
|
||||
fmt = self.FORMATS[output_type]
|
||||
if fmt == "JPEG":
|
||||
if fmt == "JPEG" or fmt == "PNG" and output_image.mode == "CMYK":
|
||||
output_image = output_image.convert("RGB")
|
||||
output_image.save(output_bytes_io, fmt, quality=80)
|
||||
return output_bytes_io
|
||||
|
||||
@@ -41,6 +41,7 @@ import attr
|
||||
from prometheus_client import Counter
|
||||
|
||||
from twisted.internet import defer
|
||||
from twisted.internet.defer import Deferred
|
||||
|
||||
from synapse.api.constants import EduTypes, EventTypes, HistoryVisibility, Membership
|
||||
from synapse.api.errors import AuthError
|
||||
@@ -52,6 +53,7 @@ from synapse.logging.opentracing import log_kv, start_active_span
|
||||
from synapse.metrics import LaterGauge
|
||||
from synapse.streams.config import PaginationConfig
|
||||
from synapse.types import (
|
||||
ISynapseReactor,
|
||||
JsonDict,
|
||||
MultiWriterStreamToken,
|
||||
PersistedEventPosition,
|
||||
@@ -61,8 +63,11 @@ from synapse.types import (
|
||||
StreamToken,
|
||||
UserID,
|
||||
)
|
||||
from synapse.util.async_helpers import ObservableDeferred, timeout_deferred
|
||||
from synapse.util.async_helpers import (
|
||||
timeout_deferred,
|
||||
)
|
||||
from synapse.util.metrics import Measure
|
||||
from synapse.util.stringutils import shortstr
|
||||
from synapse.visibility import filter_events_for_client
|
||||
|
||||
if TYPE_CHECKING:
|
||||
@@ -89,18 +94,6 @@ def count(func: Callable[[T], bool], it: Iterable[T]) -> int:
|
||||
return n
|
||||
|
||||
|
||||
class _NotificationListener:
|
||||
"""This represents a single client connection to the events stream.
|
||||
The events stream handler will have yielded to the deferred, so to
|
||||
notify the handler it is sufficient to resolve the deferred.
|
||||
"""
|
||||
|
||||
__slots__ = ["deferred"]
|
||||
|
||||
def __init__(self, deferred: "defer.Deferred"):
|
||||
self.deferred = deferred
|
||||
|
||||
|
||||
class _NotifierUserStream:
|
||||
"""This represents a user connected to the event stream.
|
||||
It tracks the most recent stream token for that user.
|
||||
@@ -113,59 +106,49 @@ class _NotifierUserStream:
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
reactor: ISynapseReactor,
|
||||
user_id: str,
|
||||
rooms: StrCollection,
|
||||
current_token: StreamToken,
|
||||
time_now_ms: int,
|
||||
):
|
||||
self.reactor = reactor
|
||||
self.user_id = user_id
|
||||
self.rooms = set(rooms)
|
||||
self.current_token = current_token
|
||||
|
||||
# The last token for which we should wake up any streams that have a
|
||||
# token that comes before it. This gets updated every time we get poked.
|
||||
# We start it at the current token since if we get any streams
|
||||
# that have a token from before we have no idea whether they should be
|
||||
# woken up or not, so lets just wake them up.
|
||||
self.last_notified_token = current_token
|
||||
self.current_token = current_token
|
||||
self.last_notified_ms = time_now_ms
|
||||
|
||||
self.notify_deferred: ObservableDeferred[StreamToken] = ObservableDeferred(
|
||||
defer.Deferred()
|
||||
)
|
||||
# Set of listeners that we need to wake up when there has been a change.
|
||||
self.listeners: Set[Deferred[StreamToken]] = set()
|
||||
|
||||
def notify(
|
||||
def update_and_fetch_deferreds(
|
||||
self,
|
||||
stream_key: StreamKeyType,
|
||||
stream_id: Union[int, RoomStreamToken, MultiWriterStreamToken],
|
||||
current_token: StreamToken,
|
||||
time_now_ms: int,
|
||||
) -> None:
|
||||
"""Notify any listeners for this user of a new event from an
|
||||
event source.
|
||||
) -> Collection["Deferred[StreamToken]"]:
|
||||
"""Update the stream for this user because of a new event from an
|
||||
event source, and return the set of deferreds to wake up.
|
||||
|
||||
Args:
|
||||
stream_key: The stream the event came from.
|
||||
stream_id: The new id for the stream the event came from.
|
||||
current_token: The new current token.
|
||||
time_now_ms: The current time in milliseconds.
|
||||
|
||||
Returns:
|
||||
The set of deferreds that need to be called.
|
||||
"""
|
||||
self.current_token = self.current_token.copy_and_advance(stream_key, stream_id)
|
||||
self.last_notified_token = self.current_token
|
||||
self.current_token = current_token
|
||||
self.last_notified_ms = time_now_ms
|
||||
notify_deferred = self.notify_deferred
|
||||
|
||||
log_kv(
|
||||
{
|
||||
"notify": self.user_id,
|
||||
"stream": stream_key,
|
||||
"stream_id": stream_id,
|
||||
"listeners": self.count_listeners(),
|
||||
}
|
||||
)
|
||||
listeners = self.listeners
|
||||
self.listeners = set()
|
||||
|
||||
users_woken_by_stream_counter.labels(stream_key).inc()
|
||||
|
||||
with PreserveLoggingContext():
|
||||
self.notify_deferred = ObservableDeferred(defer.Deferred())
|
||||
notify_deferred.callback(self.current_token)
|
||||
return listeners
|
||||
|
||||
def remove(self, notifier: "Notifier") -> None:
|
||||
"""Remove this listener from all the indexes in the Notifier
|
||||
@@ -179,9 +162,9 @@ class _NotifierUserStream:
|
||||
notifier.user_to_user_stream.pop(self.user_id)
|
||||
|
||||
def count_listeners(self) -> int:
|
||||
return len(self.notify_deferred.observers())
|
||||
return len(self.listeners)
|
||||
|
||||
def new_listener(self, token: StreamToken) -> _NotificationListener:
|
||||
def new_listener(self, token: StreamToken) -> "Deferred[StreamToken]":
|
||||
"""Returns a deferred that is resolved when there is a new token
|
||||
greater than the given token.
|
||||
|
||||
@@ -191,10 +174,17 @@ class _NotifierUserStream:
|
||||
"""
|
||||
# Immediately wake up stream if something has already since happened
|
||||
# since their last token.
|
||||
if self.last_notified_token != token:
|
||||
return _NotificationListener(defer.succeed(self.current_token))
|
||||
else:
|
||||
return _NotificationListener(self.notify_deferred.observe())
|
||||
if token != self.current_token:
|
||||
return defer.succeed(self.current_token)
|
||||
|
||||
# Create a new deferred and add it to the set of listeners. We add a
|
||||
# cancel handler to remove it from the set again, to handle timeouts.
|
||||
deferred: "Deferred[StreamToken]" = Deferred(
|
||||
canceller=lambda d: self.listeners.discard(d)
|
||||
)
|
||||
self.listeners.add(deferred)
|
||||
|
||||
return deferred
|
||||
|
||||
|
||||
@attr.s(slots=True, frozen=True, auto_attribs=True)
|
||||
@@ -247,6 +237,7 @@ class Notifier:
|
||||
# List of callbacks to be notified when a lock is released
|
||||
self._lock_released_callback: List[Callable[[str, str, str], None]] = []
|
||||
|
||||
self.reactor = hs.get_reactor()
|
||||
self.clock = hs.get_clock()
|
||||
self.appservice_handler = hs.get_application_service_handler()
|
||||
self._pusher_pool = hs.get_pusherpool()
|
||||
@@ -342,14 +333,25 @@ class Notifier:
|
||||
# Wake up all related user stream notifiers
|
||||
user_streams = self.room_to_user_streams.get(room_id, set())
|
||||
time_now_ms = self.clock.time_msec()
|
||||
current_token = self.event_sources.get_current_token()
|
||||
|
||||
listeners: List["Deferred[StreamToken]"] = []
|
||||
for user_stream in user_streams:
|
||||
try:
|
||||
user_stream.notify(
|
||||
StreamKeyType.UN_PARTIAL_STATED_ROOMS, new_token, time_now_ms
|
||||
listeners.extend(
|
||||
user_stream.update_and_fetch_deferreds(current_token, time_now_ms)
|
||||
)
|
||||
except Exception:
|
||||
logger.exception("Failed to notify listener")
|
||||
|
||||
with PreserveLoggingContext():
|
||||
for listener in listeners:
|
||||
listener.callback(current_token)
|
||||
|
||||
users_woken_by_stream_counter.labels(StreamKeyType.UN_PARTIAL_STATED_ROOMS).inc(
|
||||
len(user_streams)
|
||||
)
|
||||
|
||||
# Poke the replication so that other workers also see the write to
|
||||
# the un-partial-stated rooms stream.
|
||||
self.notify_replication()
|
||||
@@ -519,12 +521,16 @@ class Notifier:
|
||||
rooms = rooms or []
|
||||
|
||||
with Measure(self.clock, "on_new_event"):
|
||||
user_streams = set()
|
||||
user_streams: Set[_NotifierUserStream] = set()
|
||||
|
||||
log_kv(
|
||||
{
|
||||
"waking_up_explicit_users": len(users),
|
||||
"waking_up_explicit_rooms": len(rooms),
|
||||
"users": shortstr(users),
|
||||
"rooms": shortstr(rooms),
|
||||
"stream": stream_key,
|
||||
"stream_id": new_token,
|
||||
}
|
||||
)
|
||||
|
||||
@@ -544,12 +550,27 @@ class Notifier:
|
||||
)
|
||||
|
||||
time_now_ms = self.clock.time_msec()
|
||||
current_token = self.event_sources.get_current_token()
|
||||
listeners: List["Deferred[StreamToken]"] = []
|
||||
for user_stream in user_streams:
|
||||
try:
|
||||
user_stream.notify(stream_key, new_token, time_now_ms)
|
||||
listeners.extend(
|
||||
user_stream.update_and_fetch_deferreds(
|
||||
current_token, time_now_ms
|
||||
)
|
||||
)
|
||||
except Exception:
|
||||
logger.exception("Failed to notify listener")
|
||||
|
||||
# We resolve all these deferreds in one go so that we only need to
|
||||
# call `PreserveLoggingContext` once, as it has a bunch of overhead
|
||||
# (to calculate performance stats)
|
||||
with PreserveLoggingContext():
|
||||
for listener in listeners:
|
||||
listener.callback(current_token)
|
||||
|
||||
users_woken_by_stream_counter.labels(stream_key).inc(len(user_streams))
|
||||
|
||||
self.notify_replication()
|
||||
|
||||
# Notify appservices.
|
||||
@@ -586,6 +607,7 @@ class Notifier:
|
||||
if room_ids is None:
|
||||
room_ids = await self.store.get_rooms_for_user(user_id)
|
||||
user_stream = _NotifierUserStream(
|
||||
reactor=self.reactor,
|
||||
user_id=user_id,
|
||||
rooms=room_ids,
|
||||
current_token=current_token,
|
||||
@@ -608,8 +630,8 @@ class Notifier:
|
||||
# Now we wait for the _NotifierUserStream to be told there
|
||||
# is a new token.
|
||||
listener = user_stream.new_listener(prev_token)
|
||||
listener.deferred = timeout_deferred(
|
||||
listener.deferred,
|
||||
listener = timeout_deferred(
|
||||
listener,
|
||||
(end_time - now) / 1000.0,
|
||||
self.hs.get_reactor(),
|
||||
)
|
||||
@@ -622,7 +644,7 @@ class Notifier:
|
||||
)
|
||||
|
||||
with PreserveLoggingContext():
|
||||
await listener.deferred
|
||||
await listener
|
||||
|
||||
log_kv(
|
||||
{
|
||||
|
||||
@@ -436,6 +436,7 @@ class BulkPushRuleEvaluator:
|
||||
self._related_event_match_enabled,
|
||||
event.room_version.msc3931_push_features,
|
||||
self.hs.config.experimental.msc1767_enabled, # MSC3931 flag
|
||||
self.hs.config.experimental.msc4210_enabled,
|
||||
)
|
||||
|
||||
for uid, rules in rules_by_user.items():
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
#
|
||||
# This file is licensed under the Affero General Public License (AGPL) version 3.
|
||||
#
|
||||
# Copyright (C) 2023 New Vector, Ltd
|
||||
# Copyright (C) 2023-2024 New Vector, Ltd
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU Affero General Public License as
|
||||
@@ -23,6 +23,7 @@ from typing import TYPE_CHECKING
|
||||
from synapse.http.server import JsonResource
|
||||
from synapse.replication.http import (
|
||||
account_data,
|
||||
delayed_events,
|
||||
devices,
|
||||
federation,
|
||||
login,
|
||||
@@ -64,3 +65,4 @@ class ReplicationRestResource(JsonResource):
|
||||
login.register_servlets(hs, self)
|
||||
register.register_servlets(hs, self)
|
||||
devices.register_servlets(hs, self)
|
||||
delayed_events.register_servlets(hs, self)
|
||||
|
||||
62
synapse/replication/http/delayed_events.py
Normal file
62
synapse/replication/http/delayed_events.py
Normal file
@@ -0,0 +1,62 @@
|
||||
#
|
||||
# This file is licensed under the Affero General Public License (AGPL) version 3.
|
||||
#
|
||||
# Copyright (C) 2024 New Vector, Ltd
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU Affero General Public License as
|
||||
# published by the Free Software Foundation, either version 3 of the
|
||||
# License, or (at your option) any later version.
|
||||
#
|
||||
# See the GNU Affero General Public License for more details:
|
||||
# <https://www.gnu.org/licenses/agpl-3.0.html>.
|
||||
#
|
||||
|
||||
import logging
|
||||
from typing import TYPE_CHECKING, Dict, Optional, Tuple
|
||||
|
||||
from twisted.web.server import Request
|
||||
|
||||
from synapse.http.server import HttpServer
|
||||
from synapse.replication.http._base import ReplicationEndpoint
|
||||
from synapse.types import JsonDict, JsonMapping
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from synapse.server import HomeServer
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class ReplicationAddedDelayedEventRestServlet(ReplicationEndpoint):
|
||||
"""Handle a delayed event being added by another worker.
|
||||
|
||||
Request format:
|
||||
|
||||
POST /_synapse/replication/delayed_event_added/
|
||||
|
||||
{}
|
||||
"""
|
||||
|
||||
NAME = "added_delayed_event"
|
||||
PATH_ARGS = ()
|
||||
CACHE = False
|
||||
|
||||
def __init__(self, hs: "HomeServer"):
|
||||
super().__init__(hs)
|
||||
|
||||
self.handler = hs.get_delayed_events_handler()
|
||||
|
||||
@staticmethod
|
||||
async def _serialize_payload(next_send_ts: int) -> JsonDict: # type: ignore[override]
|
||||
return {"next_send_ts": next_send_ts}
|
||||
|
||||
async def _handle_request( # type: ignore[override]
|
||||
self, request: Request, content: JsonDict
|
||||
) -> Tuple[int, Dict[str, Optional[JsonMapping]]]:
|
||||
self.handler.on_added(int(content["next_send_ts"]))
|
||||
|
||||
return 200, {}
|
||||
|
||||
|
||||
def register_servlets(hs: "HomeServer", http_server: HttpServer) -> None:
|
||||
ReplicationAddedDelayedEventRestServlet(hs).register(http_server)
|
||||
@@ -48,7 +48,7 @@ class ReplicationRemovePusherRestServlet(ReplicationEndpoint):
|
||||
|
||||
"""
|
||||
|
||||
NAME = "add_user_account_data"
|
||||
NAME = "remove_pusher"
|
||||
PATH_ARGS = ("user_id",)
|
||||
CACHE = False
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
# This file is licensed under the Affero General Public License (AGPL) version 3.
|
||||
#
|
||||
# Copyright 2014-2016 OpenMarket Ltd
|
||||
# Copyright (C) 2023 New Vector, Ltd
|
||||
# Copyright (C) 2023-2024 New Vector, Ltd
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU Affero General Public License as
|
||||
@@ -31,6 +31,7 @@ from synapse.rest.client import (
|
||||
auth,
|
||||
auth_issuer,
|
||||
capabilities,
|
||||
delayed_events,
|
||||
devices,
|
||||
directory,
|
||||
events,
|
||||
@@ -81,6 +82,7 @@ CLIENT_SERVLET_FUNCTIONS: Tuple[RegisterServletsFunc, ...] = (
|
||||
room.register_deprecated_servlets,
|
||||
events.register_servlets,
|
||||
room.register_servlets,
|
||||
delayed_events.register_servlets,
|
||||
login.register_servlets,
|
||||
profile.register_servlets,
|
||||
presence.register_servlets,
|
||||
|
||||
@@ -98,6 +98,8 @@ from synapse.rest.admin.users import (
|
||||
DeactivateAccountRestServlet,
|
||||
PushersRestServlet,
|
||||
RateLimitRestServlet,
|
||||
RedactUser,
|
||||
RedactUserStatus,
|
||||
ResetPasswordRestServlet,
|
||||
SearchUsersRestServlet,
|
||||
ShadowBanRestServlet,
|
||||
@@ -319,6 +321,8 @@ def register_servlets(hs: "HomeServer", http_server: HttpServer) -> None:
|
||||
UserReplaceMasterCrossSigningKeyRestServlet(hs).register(http_server)
|
||||
UserByExternalId(hs).register(http_server)
|
||||
UserByThreePid(hs).register(http_server)
|
||||
RedactUser(hs).register(http_server)
|
||||
RedactUserStatus(hs).register(http_server)
|
||||
|
||||
DeviceRestServlet(hs).register(http_server)
|
||||
DevicesRestServlet(hs).register(http_server)
|
||||
|
||||
@@ -27,7 +27,7 @@ from typing import TYPE_CHECKING, Dict, List, Optional, Tuple, Union
|
||||
|
||||
import attr
|
||||
|
||||
from synapse._pydantic_compat import HAS_PYDANTIC_V2
|
||||
from synapse._pydantic_compat import StrictBool, StrictInt, StrictStr
|
||||
from synapse.api.constants import Direction, UserTypes
|
||||
from synapse.api.errors import Codes, NotFoundError, SynapseError
|
||||
from synapse.http.servlet import (
|
||||
@@ -50,17 +50,12 @@ from synapse.rest.admin._base import (
|
||||
from synapse.rest.client._base import client_patterns
|
||||
from synapse.storage.databases.main.registration import ExternalIDReuseException
|
||||
from synapse.storage.databases.main.stats import UserSortOrder
|
||||
from synapse.types import JsonDict, JsonMapping, UserID
|
||||
from synapse.types import JsonDict, JsonMapping, TaskStatus, UserID
|
||||
from synapse.types.rest import RequestBodyModel
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from synapse.server import HomeServer
|
||||
|
||||
if TYPE_CHECKING or HAS_PYDANTIC_V2:
|
||||
from pydantic.v1 import StrictBool
|
||||
else:
|
||||
from pydantic import StrictBool
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -1410,3 +1405,99 @@ class UserByThreePid(RestServlet):
|
||||
raise NotFoundError("User not found")
|
||||
|
||||
return HTTPStatus.OK, {"user_id": user_id}
|
||||
|
||||
|
||||
class RedactUser(RestServlet):
|
||||
"""
|
||||
Redact all the events of a given user in the given rooms or if empty dict is provided
|
||||
then all events in all rooms user is member of. Kicks off a background process and
|
||||
returns an id that can be used to check on the progress of the redaction progress
|
||||
"""
|
||||
|
||||
PATTERNS = admin_patterns("/user/(?P<user_id>[^/]*)/redact")
|
||||
|
||||
def __init__(self, hs: "HomeServer"):
|
||||
self._auth = hs.get_auth()
|
||||
self._store = hs.get_datastores().main
|
||||
self.admin_handler = hs.get_admin_handler()
|
||||
|
||||
class PostBody(RequestBodyModel):
|
||||
rooms: List[StrictStr]
|
||||
reason: Optional[StrictStr]
|
||||
limit: Optional[StrictInt]
|
||||
|
||||
async def on_POST(
|
||||
self, request: SynapseRequest, user_id: str
|
||||
) -> Tuple[int, JsonDict]:
|
||||
requester = await self._auth.get_user_by_req(request)
|
||||
await assert_user_is_admin(self._auth, requester)
|
||||
|
||||
# parse provided user id to check that it is valid
|
||||
UserID.from_string(user_id)
|
||||
|
||||
body = parse_and_validate_json_object_from_request(request, self.PostBody)
|
||||
|
||||
limit = body.limit
|
||||
if limit and limit <= 0:
|
||||
raise SynapseError(
|
||||
HTTPStatus.BAD_REQUEST,
|
||||
"If limit is provided it must be a non-negative integer greater than 0.",
|
||||
)
|
||||
|
||||
rooms = body.rooms
|
||||
if not rooms:
|
||||
current_rooms = list(await self._store.get_rooms_for_user(user_id))
|
||||
banned_rooms = list(
|
||||
await self._store.get_rooms_user_currently_banned_from(user_id)
|
||||
)
|
||||
rooms = current_rooms + banned_rooms
|
||||
|
||||
redact_id = await self.admin_handler.start_redact_events(
|
||||
user_id, rooms, requester.serialize(), body.reason, limit
|
||||
)
|
||||
|
||||
return HTTPStatus.OK, {"redact_id": redact_id}
|
||||
|
||||
|
||||
class RedactUserStatus(RestServlet):
|
||||
"""
|
||||
Check on the progress of the redaction request represented by the provided ID, returning
|
||||
the status of the process and a dict of events that were unable to be redacted, if any
|
||||
"""
|
||||
|
||||
PATTERNS = admin_patterns("/user/redact_status/(?P<redact_id>[^/]*)$")
|
||||
|
||||
def __init__(self, hs: "HomeServer"):
|
||||
self._auth = hs.get_auth()
|
||||
self.admin_handler = hs.get_admin_handler()
|
||||
|
||||
async def on_GET(
|
||||
self, request: SynapseRequest, redact_id: str
|
||||
) -> Tuple[int, JsonDict]:
|
||||
await assert_requester_is_admin(self._auth, request)
|
||||
|
||||
task = await self.admin_handler.get_redact_task(redact_id)
|
||||
|
||||
if task:
|
||||
if task.status == TaskStatus.ACTIVE:
|
||||
return HTTPStatus.OK, {"status": TaskStatus.ACTIVE}
|
||||
elif task.status == TaskStatus.COMPLETE:
|
||||
assert task.result is not None
|
||||
failed_redactions = task.result.get("failed_redactions")
|
||||
return HTTPStatus.OK, {
|
||||
"status": TaskStatus.COMPLETE,
|
||||
"failed_redactions": failed_redactions if failed_redactions else {},
|
||||
}
|
||||
elif task.status == TaskStatus.SCHEDULED:
|
||||
return HTTPStatus.OK, {"status": TaskStatus.SCHEDULED}
|
||||
else:
|
||||
return HTTPStatus.OK, {
|
||||
"status": TaskStatus.FAILED,
|
||||
"error": (
|
||||
task.error
|
||||
if task.error
|
||||
else "Unknown error, please check the logs for more information."
|
||||
),
|
||||
}
|
||||
else:
|
||||
raise NotFoundError("redact id '%s' not found" % redact_id)
|
||||
|
||||
@@ -24,18 +24,12 @@ import random
|
||||
from typing import TYPE_CHECKING, List, Optional, Tuple
|
||||
from urllib.parse import urlparse
|
||||
|
||||
from synapse._pydantic_compat import HAS_PYDANTIC_V2
|
||||
|
||||
if TYPE_CHECKING or HAS_PYDANTIC_V2:
|
||||
from pydantic.v1 import StrictBool, StrictStr, constr
|
||||
else:
|
||||
from pydantic import StrictBool, StrictStr, constr
|
||||
|
||||
import attr
|
||||
from typing_extensions import Literal
|
||||
|
||||
from twisted.web.server import Request
|
||||
|
||||
from synapse._pydantic_compat import StrictBool, StrictStr, constr
|
||||
from synapse.api.constants import LoginType
|
||||
from synapse.api.errors import (
|
||||
Codes,
|
||||
|
||||
111
synapse/rest/client/delayed_events.py
Normal file
111
synapse/rest/client/delayed_events.py
Normal file
@@ -0,0 +1,111 @@
|
||||
#
|
||||
# This file is licensed under the Affero General Public License (AGPL) version 3.
|
||||
#
|
||||
# Copyright (C) 2024 New Vector, Ltd
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU Affero General Public License as
|
||||
# published by the Free Software Foundation, either version 3 of the
|
||||
# License, or (at your option) any later version.
|
||||
#
|
||||
# See the GNU Affero General Public License for more details:
|
||||
# <https://www.gnu.org/licenses/agpl-3.0.html>.
|
||||
#
|
||||
|
||||
# This module contains REST servlets to do with delayed events: /delayed_events/<paths>
|
||||
|
||||
import logging
|
||||
from enum import Enum
|
||||
from http import HTTPStatus
|
||||
from typing import TYPE_CHECKING, Tuple
|
||||
|
||||
from synapse.api.errors import Codes, SynapseError
|
||||
from synapse.http.server import HttpServer
|
||||
from synapse.http.servlet import RestServlet, parse_json_object_from_request
|
||||
from synapse.http.site import SynapseRequest
|
||||
from synapse.rest.client._base import client_patterns
|
||||
from synapse.types import JsonDict
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from synapse.server import HomeServer
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class _UpdateDelayedEventAction(Enum):
|
||||
CANCEL = "cancel"
|
||||
RESTART = "restart"
|
||||
SEND = "send"
|
||||
|
||||
|
||||
class UpdateDelayedEventServlet(RestServlet):
|
||||
PATTERNS = client_patterns(
|
||||
r"/org\.matrix\.msc4140/delayed_events/(?P<delay_id>[^/]+)$",
|
||||
releases=(),
|
||||
)
|
||||
CATEGORY = "Delayed event management requests"
|
||||
|
||||
def __init__(self, hs: "HomeServer"):
|
||||
super().__init__()
|
||||
self.auth = hs.get_auth()
|
||||
self.delayed_events_handler = hs.get_delayed_events_handler()
|
||||
|
||||
async def on_POST(
|
||||
self, request: SynapseRequest, delay_id: str
|
||||
) -> Tuple[int, JsonDict]:
|
||||
requester = await self.auth.get_user_by_req(request)
|
||||
|
||||
body = parse_json_object_from_request(request)
|
||||
try:
|
||||
action = str(body["action"])
|
||||
except KeyError:
|
||||
raise SynapseError(
|
||||
HTTPStatus.BAD_REQUEST,
|
||||
"'action' is missing",
|
||||
Codes.MISSING_PARAM,
|
||||
)
|
||||
try:
|
||||
enum_action = _UpdateDelayedEventAction(action)
|
||||
except ValueError:
|
||||
raise SynapseError(
|
||||
HTTPStatus.BAD_REQUEST,
|
||||
"'action' is not one of "
|
||||
+ ", ".join(f"'{m.value}'" for m in _UpdateDelayedEventAction),
|
||||
Codes.INVALID_PARAM,
|
||||
)
|
||||
|
||||
if enum_action == _UpdateDelayedEventAction.CANCEL:
|
||||
await self.delayed_events_handler.cancel(requester, delay_id)
|
||||
elif enum_action == _UpdateDelayedEventAction.RESTART:
|
||||
await self.delayed_events_handler.restart(requester, delay_id)
|
||||
elif enum_action == _UpdateDelayedEventAction.SEND:
|
||||
await self.delayed_events_handler.send(requester, delay_id)
|
||||
return 200, {}
|
||||
|
||||
|
||||
class DelayedEventsServlet(RestServlet):
|
||||
PATTERNS = client_patterns(
|
||||
r"/org\.matrix\.msc4140/delayed_events$",
|
||||
releases=(),
|
||||
)
|
||||
CATEGORY = "Delayed event management requests"
|
||||
|
||||
def __init__(self, hs: "HomeServer"):
|
||||
super().__init__()
|
||||
self.auth = hs.get_auth()
|
||||
self.delayed_events_handler = hs.get_delayed_events_handler()
|
||||
|
||||
async def on_GET(self, request: SynapseRequest) -> Tuple[int, JsonDict]:
|
||||
requester = await self.auth.get_user_by_req(request)
|
||||
# TODO: Support Pagination stream API ("from" query parameter)
|
||||
delayed_events = await self.delayed_events_handler.get_all_for_user(requester)
|
||||
|
||||
ret = {"delayed_events": delayed_events}
|
||||
return 200, ret
|
||||
|
||||
|
||||
def register_servlets(hs: "HomeServer", http_server: HttpServer) -> None:
|
||||
# The following can't currently be instantiated on workers.
|
||||
if hs.config.worker.worker_app is None:
|
||||
UpdateDelayedEventServlet(hs).register(http_server)
|
||||
DelayedEventsServlet(hs).register(http_server)
|
||||
@@ -24,13 +24,7 @@ import logging
|
||||
from http import HTTPStatus
|
||||
from typing import TYPE_CHECKING, List, Optional, Tuple
|
||||
|
||||
from synapse._pydantic_compat import HAS_PYDANTIC_V2
|
||||
|
||||
if TYPE_CHECKING or HAS_PYDANTIC_V2:
|
||||
from pydantic.v1 import Extra, StrictStr
|
||||
else:
|
||||
from pydantic import Extra, StrictStr
|
||||
|
||||
from synapse._pydantic_compat import Extra, StrictStr
|
||||
from synapse.api import errors
|
||||
from synapse.api.errors import NotFoundError, SynapseError, UnrecognizedRequestError
|
||||
from synapse.handlers.device import DeviceHandler
|
||||
|
||||
@@ -22,17 +22,11 @@
|
||||
import logging
|
||||
from typing import TYPE_CHECKING, List, Optional, Tuple
|
||||
|
||||
from synapse._pydantic_compat import HAS_PYDANTIC_V2
|
||||
|
||||
if TYPE_CHECKING or HAS_PYDANTIC_V2:
|
||||
from pydantic.v1 import StrictStr
|
||||
else:
|
||||
from pydantic import StrictStr
|
||||
|
||||
from typing_extensions import Literal
|
||||
|
||||
from twisted.web.server import Request
|
||||
|
||||
from synapse._pydantic_compat import StrictStr
|
||||
from synapse.api.errors import AuthError, Codes, NotFoundError, SynapseError
|
||||
from synapse.http.server import HttpServer
|
||||
from synapse.http.servlet import (
|
||||
|
||||
@@ -363,6 +363,7 @@ class LoginRestServlet(RestServlet):
|
||||
login_submission: JsonDict,
|
||||
callback: Optional[Callable[[LoginResponse], Awaitable[None]]] = None,
|
||||
create_non_existent_users: bool = False,
|
||||
default_display_name: Optional[str] = None,
|
||||
ratelimit: bool = True,
|
||||
auth_provider_id: Optional[str] = None,
|
||||
should_issue_refresh_token: bool = False,
|
||||
@@ -410,7 +411,8 @@ class LoginRestServlet(RestServlet):
|
||||
canonical_uid = await self.auth_handler.check_user_exists(user_id)
|
||||
if not canonical_uid:
|
||||
canonical_uid = await self.registration_handler.register_user(
|
||||
localpart=UserID.from_string(user_id).localpart
|
||||
localpart=UserID.from_string(user_id).localpart,
|
||||
default_display_name=default_display_name,
|
||||
)
|
||||
user_id = canonical_uid
|
||||
|
||||
@@ -546,11 +548,14 @@ class LoginRestServlet(RestServlet):
|
||||
Returns:
|
||||
The body of the JSON response.
|
||||
"""
|
||||
user_id = self.hs.get_jwt_handler().validate_login(login_submission)
|
||||
user_id, default_display_name = self.hs.get_jwt_handler().validate_login(
|
||||
login_submission
|
||||
)
|
||||
return await self._complete_login(
|
||||
user_id,
|
||||
login_submission,
|
||||
create_non_existent_users=True,
|
||||
default_display_name=default_display_name,
|
||||
should_issue_refresh_token=should_issue_refresh_token,
|
||||
request_info=request_info,
|
||||
)
|
||||
|
||||
@@ -23,7 +23,7 @@ import logging
|
||||
from http import HTTPStatus
|
||||
from typing import TYPE_CHECKING, Tuple
|
||||
|
||||
from synapse._pydantic_compat import HAS_PYDANTIC_V2
|
||||
from synapse._pydantic_compat import StrictStr
|
||||
from synapse.api.errors import AuthError, Codes, NotFoundError, SynapseError
|
||||
from synapse.http.server import HttpServer
|
||||
from synapse.http.servlet import (
|
||||
@@ -40,10 +40,6 @@ from ._base import client_patterns
|
||||
if TYPE_CHECKING:
|
||||
from synapse.server import HomeServer
|
||||
|
||||
if TYPE_CHECKING or HAS_PYDANTIC_V2:
|
||||
from pydantic.v1 import StrictStr
|
||||
else:
|
||||
from pydantic import StrictStr
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
# This file is licensed under the Affero General Public License (AGPL) version 3.
|
||||
#
|
||||
# Copyright 2014-2016 OpenMarket Ltd
|
||||
# Copyright (C) 2023 New Vector, Ltd
|
||||
# Copyright (C) 2023-2024 New Vector, Ltd
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU Affero General Public License as
|
||||
@@ -195,7 +195,9 @@ class RoomStateEventRestServlet(RestServlet):
|
||||
self.event_creation_handler = hs.get_event_creation_handler()
|
||||
self.room_member_handler = hs.get_room_member_handler()
|
||||
self.message_handler = hs.get_message_handler()
|
||||
self.delayed_events_handler = hs.get_delayed_events_handler()
|
||||
self.auth = hs.get_auth()
|
||||
self._max_event_delay_ms = hs.config.server.max_event_delay_ms
|
||||
|
||||
def register(self, http_server: HttpServer) -> None:
|
||||
# /rooms/$roomid/state/$eventtype
|
||||
@@ -291,6 +293,22 @@ class RoomStateEventRestServlet(RestServlet):
|
||||
if requester.app_service:
|
||||
origin_server_ts = parse_integer(request, "ts")
|
||||
|
||||
delay = _parse_request_delay(request, self._max_event_delay_ms)
|
||||
if delay is not None:
|
||||
delay_id = await self.delayed_events_handler.add(
|
||||
requester,
|
||||
room_id=room_id,
|
||||
event_type=event_type,
|
||||
state_key=state_key,
|
||||
origin_server_ts=origin_server_ts,
|
||||
content=content,
|
||||
delay=delay,
|
||||
)
|
||||
|
||||
set_tag("delay_id", delay_id)
|
||||
ret = {"delay_id": delay_id}
|
||||
return 200, ret
|
||||
|
||||
try:
|
||||
if event_type == EventTypes.Member:
|
||||
membership = content.get("membership", None)
|
||||
@@ -341,7 +359,9 @@ class RoomSendEventRestServlet(TransactionRestServlet):
|
||||
def __init__(self, hs: "HomeServer"):
|
||||
super().__init__(hs)
|
||||
self.event_creation_handler = hs.get_event_creation_handler()
|
||||
self.delayed_events_handler = hs.get_delayed_events_handler()
|
||||
self.auth = hs.get_auth()
|
||||
self._max_event_delay_ms = hs.config.server.max_event_delay_ms
|
||||
|
||||
def register(self, http_server: HttpServer) -> None:
|
||||
# /rooms/$roomid/send/$event_type[/$txn_id]
|
||||
@@ -358,6 +378,26 @@ class RoomSendEventRestServlet(TransactionRestServlet):
|
||||
) -> Tuple[int, JsonDict]:
|
||||
content = parse_json_object_from_request(request)
|
||||
|
||||
origin_server_ts = None
|
||||
if requester.app_service:
|
||||
origin_server_ts = parse_integer(request, "ts")
|
||||
|
||||
delay = _parse_request_delay(request, self._max_event_delay_ms)
|
||||
if delay is not None:
|
||||
delay_id = await self.delayed_events_handler.add(
|
||||
requester,
|
||||
room_id=room_id,
|
||||
event_type=event_type,
|
||||
state_key=None,
|
||||
origin_server_ts=origin_server_ts,
|
||||
content=content,
|
||||
delay=delay,
|
||||
)
|
||||
|
||||
set_tag("delay_id", delay_id)
|
||||
ret = {"delay_id": delay_id}
|
||||
return 200, ret
|
||||
|
||||
event_dict: JsonDict = {
|
||||
"type": event_type,
|
||||
"content": content,
|
||||
@@ -365,10 +405,8 @@ class RoomSendEventRestServlet(TransactionRestServlet):
|
||||
"sender": requester.user.to_string(),
|
||||
}
|
||||
|
||||
if requester.app_service:
|
||||
origin_server_ts = parse_integer(request, "ts")
|
||||
if origin_server_ts is not None:
|
||||
event_dict["origin_server_ts"] = origin_server_ts
|
||||
if origin_server_ts is not None:
|
||||
event_dict["origin_server_ts"] = origin_server_ts
|
||||
|
||||
try:
|
||||
(
|
||||
@@ -411,6 +449,49 @@ class RoomSendEventRestServlet(TransactionRestServlet):
|
||||
)
|
||||
|
||||
|
||||
def _parse_request_delay(
|
||||
request: SynapseRequest,
|
||||
max_delay: Optional[int],
|
||||
) -> Optional[int]:
|
||||
"""Parses from the request string the delay parameter for
|
||||
delayed event requests, and checks it for correctness.
|
||||
|
||||
Args:
|
||||
request: the twisted HTTP request.
|
||||
max_delay: the maximum allowed value of the delay parameter,
|
||||
or None if no delay parameter is allowed.
|
||||
Returns:
|
||||
The value of the requested delay, or None if it was absent.
|
||||
|
||||
Raises:
|
||||
SynapseError: if the delay parameter is present and forbidden,
|
||||
or if it exceeds the maximum allowed value.
|
||||
"""
|
||||
delay = parse_integer(request, "org.matrix.msc4140.delay")
|
||||
if delay is None:
|
||||
return None
|
||||
if max_delay is None:
|
||||
raise SynapseError(
|
||||
HTTPStatus.BAD_REQUEST,
|
||||
"Delayed events are not supported on this server",
|
||||
Codes.UNKNOWN,
|
||||
{
|
||||
"org.matrix.msc4140.errcode": "M_MAX_DELAY_UNSUPPORTED",
|
||||
},
|
||||
)
|
||||
if delay > max_delay:
|
||||
raise SynapseError(
|
||||
HTTPStatus.BAD_REQUEST,
|
||||
"The requested delay exceeds the allowed maximum.",
|
||||
Codes.UNKNOWN,
|
||||
{
|
||||
"org.matrix.msc4140.errcode": "M_MAX_DELAY_EXCEEDED",
|
||||
"org.matrix.msc4140.max_delay": max_delay,
|
||||
},
|
||||
)
|
||||
return delay
|
||||
|
||||
|
||||
# TODO: Needs unit testing for room ID + alias joins
|
||||
class JoinRoomAliasServlet(ResolveRoomIdMixin, TransactionRestServlet):
|
||||
CATEGORY = "Event sending requests"
|
||||
|
||||
@@ -1010,11 +1010,13 @@ class SlidingSyncRestServlet(RestServlet):
|
||||
serialized_rooms: Dict[str, JsonDict] = {}
|
||||
for room_id, room_result in rooms.items():
|
||||
serialized_rooms[room_id] = {
|
||||
"bump_stamp": room_result.bump_stamp,
|
||||
"notification_count": room_result.notification_count,
|
||||
"highlight_count": room_result.highlight_count,
|
||||
}
|
||||
|
||||
if room_result.bump_stamp is not None:
|
||||
serialized_rooms[room_id]["bump_stamp"] = room_result.bump_stamp
|
||||
|
||||
if room_result.joined_count is not None:
|
||||
serialized_rooms[room_id]["joined_count"] = room_result.joined_count
|
||||
|
||||
@@ -1044,7 +1046,7 @@ class SlidingSyncRestServlet(RestServlet):
|
||||
serialized_rooms[room_id]["heroes"] = serialized_heroes
|
||||
|
||||
# We should only include the `initial` key if it's `True` to save bandwidth.
|
||||
# The absense of this flag means `False`.
|
||||
# The absence of this flag means `False`.
|
||||
if room_result.initial:
|
||||
serialized_rooms[room_id]["initial"] = room_result.initial
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
# Copyright 2019 The Matrix.org Foundation C.I.C.
|
||||
# Copyright 2017 Vector Creations Ltd
|
||||
# Copyright 2016 OpenMarket Ltd
|
||||
# Copyright (C) 2023 New Vector, Ltd
|
||||
# Copyright (C) 2023-2024 New Vector, Ltd
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU Affero General Public License as
|
||||
@@ -171,6 +171,8 @@ class VersionsRestServlet(RestServlet):
|
||||
is not None
|
||||
)
|
||||
),
|
||||
# MSC4140: Delayed events
|
||||
"org.matrix.msc4140": bool(self.config.server.max_event_delay_ms),
|
||||
# MSC4151: Report room API (Client-Server API)
|
||||
"org.matrix.msc4151": self.config.experimental.msc4151_enabled,
|
||||
# Simplified sliding sync
|
||||
|
||||
@@ -23,17 +23,11 @@ import logging
|
||||
import re
|
||||
from typing import TYPE_CHECKING, Dict, Mapping, Optional, Set, Tuple
|
||||
|
||||
from synapse._pydantic_compat import HAS_PYDANTIC_V2
|
||||
|
||||
if TYPE_CHECKING or HAS_PYDANTIC_V2:
|
||||
from pydantic.v1 import Extra, StrictInt, StrictStr
|
||||
else:
|
||||
from pydantic import Extra, StrictInt, StrictStr
|
||||
|
||||
from signedjson.sign import sign_json
|
||||
|
||||
from twisted.web.server import Request
|
||||
|
||||
from synapse._pydantic_compat import Extra, StrictInt, StrictStr
|
||||
from synapse.crypto.keyring import ServerKeyFetcher
|
||||
from synapse.http.server import HttpServer
|
||||
from synapse.http.servlet import (
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
# This file is licensed under the Affero General Public License (AGPL) version 3.
|
||||
#
|
||||
# Copyright 2021 The Matrix.org Foundation C.I.C.
|
||||
# Copyright (C) 2023 New Vector, Ltd
|
||||
# Copyright (C) 2023-2024 New Vector, Ltd
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU Affero General Public License as
|
||||
@@ -68,6 +68,7 @@ from synapse.handlers.appservice import ApplicationServicesHandler
|
||||
from synapse.handlers.auth import AuthHandler, PasswordAuthProvider
|
||||
from synapse.handlers.cas import CasHandler
|
||||
from synapse.handlers.deactivate_account import DeactivateAccountHandler
|
||||
from synapse.handlers.delayed_events import DelayedEventsHandler
|
||||
from synapse.handlers.device import DeviceHandler, DeviceWorkerHandler
|
||||
from synapse.handlers.devicemessage import DeviceMessageHandler
|
||||
from synapse.handlers.directory import DirectoryHandler
|
||||
@@ -251,6 +252,7 @@ class HomeServer(metaclass=abc.ABCMeta):
|
||||
"account_validity",
|
||||
"auth",
|
||||
"deactivate_account",
|
||||
"delayed_events",
|
||||
"message",
|
||||
"pagination",
|
||||
"profile",
|
||||
@@ -964,3 +966,7 @@ class HomeServer(metaclass=abc.ABCMeta):
|
||||
register_threadpool("media", media_threadpool)
|
||||
|
||||
return media_threadpool
|
||||
|
||||
@cache_in_self
|
||||
def get_delayed_events_handler(self) -> DelayedEventsHandler:
|
||||
return DelayedEventsHandler(self)
|
||||
|
||||
@@ -112,6 +112,7 @@ class SQLBaseStore(metaclass=ABCMeta):
|
||||
self._attempt_to_invalidate_cache(
|
||||
"get_number_joined_users_in_room", (room_id,)
|
||||
)
|
||||
self._attempt_to_invalidate_cache("get_member_counts", (room_id,))
|
||||
self._attempt_to_invalidate_cache("get_local_users_in_room", (room_id,))
|
||||
|
||||
# There's no easy way of invalidating this cache for just the users
|
||||
@@ -135,6 +136,7 @@ class SQLBaseStore(metaclass=ABCMeta):
|
||||
self._attempt_to_invalidate_cache("get_partial_current_state_ids", (room_id,))
|
||||
self._attempt_to_invalidate_cache("get_room_type", (room_id,))
|
||||
self._attempt_to_invalidate_cache("get_room_encryption", (room_id,))
|
||||
self._attempt_to_invalidate_cache("get_sliding_sync_rooms_for_user", None)
|
||||
|
||||
def _invalidate_state_caches_all(self, room_id: str) -> None:
|
||||
"""Invalidates caches that are based on the current state, but does
|
||||
@@ -153,6 +155,7 @@ class SQLBaseStore(metaclass=ABCMeta):
|
||||
self._attempt_to_invalidate_cache("get_current_hosts_in_room", (room_id,))
|
||||
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_member_counts", (room_id,))
|
||||
self._attempt_to_invalidate_cache("get_local_users_in_room", (room_id,))
|
||||
self._attempt_to_invalidate_cache("does_pair_of_users_share_a_room", None)
|
||||
self._attempt_to_invalidate_cache("get_user_in_room_with_profile", None)
|
||||
|
||||
@@ -40,7 +40,7 @@ from typing import (
|
||||
|
||||
import attr
|
||||
|
||||
from synapse._pydantic_compat import HAS_PYDANTIC_V2
|
||||
from synapse._pydantic_compat import BaseModel
|
||||
from synapse.metrics.background_process_metrics import run_as_background_process
|
||||
from synapse.storage.engines import PostgresEngine
|
||||
from synapse.storage.types import Connection, Cursor
|
||||
@@ -49,11 +49,6 @@ from synapse.util import Clock, json_encoder
|
||||
|
||||
from . import engines
|
||||
|
||||
if TYPE_CHECKING or HAS_PYDANTIC_V2:
|
||||
from pydantic.v1 import BaseModel
|
||||
else:
|
||||
from pydantic import BaseModel
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from synapse.server import HomeServer
|
||||
from synapse.storage.database import (
|
||||
@@ -495,6 +490,12 @@ class BackgroundUpdater:
|
||||
if self._all_done:
|
||||
return True
|
||||
|
||||
# We now check if we have completed all pending background updates. We
|
||||
# do this as once this returns True then it will set `self._all_done`
|
||||
# and we can skip checking the database in future.
|
||||
if await self.has_completed_background_updates():
|
||||
return True
|
||||
|
||||
rows = await self.db_pool.simple_select_many_batch(
|
||||
table="background_updates",
|
||||
column="update_name",
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
#
|
||||
# Copyright 2019-2021 The Matrix.org Foundation C.I.C.
|
||||
# Copyright 2014-2016 OpenMarket Ltd
|
||||
# Copyright (C) 2023 New Vector, Ltd
|
||||
# Copyright (C) 2023-2024 New Vector, Ltd
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU Affero General Public License as
|
||||
@@ -44,6 +44,7 @@ from .appservice import ApplicationServiceStore, ApplicationServiceTransactionSt
|
||||
from .cache import CacheInvalidationWorkerStore
|
||||
from .censor_events import CensorEventsStore
|
||||
from .client_ips import ClientIpWorkerStore
|
||||
from .delayed_events import DelayedEventsStore
|
||||
from .deviceinbox import DeviceInboxStore
|
||||
from .devices import DeviceStore
|
||||
from .directory import DirectoryStore
|
||||
@@ -158,6 +159,7 @@ class DataStore(
|
||||
SessionStore,
|
||||
TaskSchedulerWorkerStore,
|
||||
SlidingSyncStore,
|
||||
DelayedEventsStore,
|
||||
):
|
||||
def __init__(
|
||||
self,
|
||||
|
||||
@@ -177,7 +177,7 @@ class AccountDataWorkerStore(PushRulesWorkerStore, CacheInvalidationWorkerStore)
|
||||
|
||||
def get_room_account_data_for_user_txn(
|
||||
txn: LoggingTransaction,
|
||||
) -> Dict[str, Dict[str, JsonDict]]:
|
||||
) -> Dict[str, Dict[str, JsonMapping]]:
|
||||
# The 'content != '{}' condition below prevents us from using
|
||||
# `simple_select_list_txn` here, as it doesn't support conditions
|
||||
# other than 'equals'.
|
||||
@@ -194,7 +194,7 @@ class AccountDataWorkerStore(PushRulesWorkerStore, CacheInvalidationWorkerStore)
|
||||
|
||||
txn.execute(sql, (user_id,))
|
||||
|
||||
by_room: Dict[str, Dict[str, JsonDict]] = {}
|
||||
by_room: Dict[str, Dict[str, JsonMapping]] = {}
|
||||
for room_id, account_data_type, content in txn:
|
||||
room_data = by_room.setdefault(room_id, {})
|
||||
|
||||
@@ -394,7 +394,7 @@ class AccountDataWorkerStore(PushRulesWorkerStore, CacheInvalidationWorkerStore)
|
||||
|
||||
async def get_updated_global_account_data_for_user(
|
||||
self, user_id: str, stream_id: int
|
||||
) -> Mapping[str, JsonMapping]:
|
||||
) -> Dict[str, JsonMapping]:
|
||||
"""Get all the global account_data that's changed for a user.
|
||||
|
||||
Args:
|
||||
@@ -407,7 +407,7 @@ class AccountDataWorkerStore(PushRulesWorkerStore, CacheInvalidationWorkerStore)
|
||||
|
||||
def get_updated_global_account_data_for_user(
|
||||
txn: LoggingTransaction,
|
||||
) -> Dict[str, JsonDict]:
|
||||
) -> Dict[str, JsonMapping]:
|
||||
sql = """
|
||||
SELECT account_data_type, content FROM account_data
|
||||
WHERE user_id = ? AND stream_id > ?
|
||||
@@ -429,7 +429,7 @@ class AccountDataWorkerStore(PushRulesWorkerStore, CacheInvalidationWorkerStore)
|
||||
|
||||
async def get_updated_room_account_data_for_user(
|
||||
self, user_id: str, stream_id: int
|
||||
) -> Dict[str, Dict[str, JsonDict]]:
|
||||
) -> Dict[str, Dict[str, JsonMapping]]:
|
||||
"""Get all the room account_data that's changed for a user.
|
||||
|
||||
Args:
|
||||
@@ -442,14 +442,14 @@ class AccountDataWorkerStore(PushRulesWorkerStore, CacheInvalidationWorkerStore)
|
||||
|
||||
def get_updated_room_account_data_for_user_txn(
|
||||
txn: LoggingTransaction,
|
||||
) -> Dict[str, Dict[str, JsonDict]]:
|
||||
) -> Dict[str, Dict[str, JsonMapping]]:
|
||||
sql = """
|
||||
SELECT room_id, account_data_type, content FROM room_account_data
|
||||
WHERE user_id = ? AND stream_id > ?
|
||||
"""
|
||||
txn.execute(sql, (user_id, stream_id))
|
||||
|
||||
account_data_by_room: Dict[str, Dict[str, JsonDict]] = {}
|
||||
account_data_by_room: Dict[str, Dict[str, JsonMapping]] = {}
|
||||
for row in txn:
|
||||
room_account_data = account_data_by_room.setdefault(row[0], {})
|
||||
room_account_data[row[1]] = db_to_json(row[2])
|
||||
@@ -467,6 +467,56 @@ class AccountDataWorkerStore(PushRulesWorkerStore, CacheInvalidationWorkerStore)
|
||||
get_updated_room_account_data_for_user_txn,
|
||||
)
|
||||
|
||||
async def get_updated_room_account_data_for_user_for_room(
|
||||
self,
|
||||
# Since there are multiple arguments with the same type, force keyword arguments
|
||||
# so people don't accidentally swap the order
|
||||
*,
|
||||
user_id: str,
|
||||
room_id: str,
|
||||
from_stream_id: int,
|
||||
to_stream_id: int,
|
||||
) -> Dict[str, JsonMapping]:
|
||||
"""Get the room account_data that's changed for a user in a room.
|
||||
|
||||
(> `from_stream_id` and <= `to_stream_id`)
|
||||
|
||||
Args:
|
||||
user_id: The user to get the account_data for.
|
||||
room_id: The room to check
|
||||
from_stream_id: The point in the stream to fetch from
|
||||
to_stream_id: The point in the stream to fetch to
|
||||
|
||||
Returns:
|
||||
A dict of the room account data.
|
||||
"""
|
||||
|
||||
def get_updated_room_account_data_for_user_for_room_txn(
|
||||
txn: LoggingTransaction,
|
||||
) -> Dict[str, JsonMapping]:
|
||||
sql = """
|
||||
SELECT account_data_type, content FROM room_account_data
|
||||
WHERE user_id = ? AND room_id = ? AND stream_id > ? AND stream_id <= ?
|
||||
"""
|
||||
txn.execute(sql, (user_id, room_id, from_stream_id, to_stream_id))
|
||||
|
||||
room_account_data: Dict[str, JsonMapping] = {}
|
||||
for row in txn:
|
||||
room_account_data[row[0]] = db_to_json(row[1])
|
||||
|
||||
return room_account_data
|
||||
|
||||
changed = self._account_data_stream_cache.has_entity_changed(
|
||||
user_id, int(from_stream_id)
|
||||
)
|
||||
if not changed:
|
||||
return {}
|
||||
|
||||
return await self.db_pool.runInteraction(
|
||||
"get_updated_room_account_data_for_user_for_room",
|
||||
get_updated_room_account_data_for_user_for_room_txn,
|
||||
)
|
||||
|
||||
@cached(max_entries=5000, iterable=True)
|
||||
async def ignored_by(self, user_id: str) -> FrozenSet[str]:
|
||||
"""
|
||||
|
||||
@@ -41,6 +41,7 @@ from synapse.storage.database import (
|
||||
LoggingDatabaseConnection,
|
||||
LoggingTransaction,
|
||||
)
|
||||
from synapse.storage.databases.main.events import SLIDING_SYNC_RELEVANT_STATE_SET
|
||||
from synapse.storage.engines import PostgresEngine
|
||||
from synapse.storage.util.id_generators import MultiWriterIdGenerator
|
||||
from synapse.util.caches.descriptors import CachedFunction
|
||||
@@ -271,12 +272,20 @@ class CacheInvalidationWorkerStore(SQLBaseStore):
|
||||
self._attempt_to_invalidate_cache(
|
||||
"get_rooms_for_user", (data.state_key,)
|
||||
)
|
||||
self._attempt_to_invalidate_cache(
|
||||
"get_sliding_sync_rooms_for_user", None
|
||||
)
|
||||
elif data.type == EventTypes.RoomEncryption:
|
||||
self._attempt_to_invalidate_cache(
|
||||
"get_room_encryption", (data.room_id,)
|
||||
)
|
||||
elif data.type == EventTypes.Create:
|
||||
self._attempt_to_invalidate_cache("get_room_type", (data.room_id,))
|
||||
|
||||
if (data.type, data.state_key) in SLIDING_SYNC_RELEVANT_STATE_SET:
|
||||
self._attempt_to_invalidate_cache(
|
||||
"get_sliding_sync_rooms_for_user", None
|
||||
)
|
||||
elif row.type == EventsStreamAllStateRow.TypeId:
|
||||
assert isinstance(data, EventsStreamAllStateRow)
|
||||
# Similar to the above, but the entire caches are invalidated. This is
|
||||
@@ -285,6 +294,7 @@ class CacheInvalidationWorkerStore(SQLBaseStore):
|
||||
self._attempt_to_invalidate_cache("get_rooms_for_user", None)
|
||||
self._attempt_to_invalidate_cache("get_room_type", (data.room_id,))
|
||||
self._attempt_to_invalidate_cache("get_room_encryption", (data.room_id,))
|
||||
self._attempt_to_invalidate_cache("get_sliding_sync_rooms_for_user", None)
|
||||
else:
|
||||
raise Exception("Unknown events stream row type %s" % (row.type,))
|
||||
|
||||
@@ -365,6 +375,9 @@ class CacheInvalidationWorkerStore(SQLBaseStore):
|
||||
elif etype == EventTypes.RoomEncryption:
|
||||
self._attempt_to_invalidate_cache("get_room_encryption", (room_id,))
|
||||
|
||||
if (etype, state_key) in SLIDING_SYNC_RELEVANT_STATE_SET:
|
||||
self._attempt_to_invalidate_cache("get_sliding_sync_rooms_for_user", None)
|
||||
|
||||
if relates_to:
|
||||
self._attempt_to_invalidate_cache(
|
||||
"get_relations_for_event",
|
||||
@@ -458,6 +471,7 @@ class CacheInvalidationWorkerStore(SQLBaseStore):
|
||||
|
||||
self._attempt_to_invalidate_cache("get_account_data_for_room", None)
|
||||
self._attempt_to_invalidate_cache("get_account_data_for_room_and_type", None)
|
||||
self._attempt_to_invalidate_cache("get_tags_for_room", None)
|
||||
self._attempt_to_invalidate_cache("get_aliases_for_room", (room_id,))
|
||||
self._attempt_to_invalidate_cache("get_latest_event_ids_in_room", (room_id,))
|
||||
self._attempt_to_invalidate_cache("_get_forward_extremeties_for_room", None)
|
||||
@@ -477,6 +491,7 @@ class CacheInvalidationWorkerStore(SQLBaseStore):
|
||||
self._attempt_to_invalidate_cache(
|
||||
"get_current_hosts_in_room_ordered", (room_id,)
|
||||
)
|
||||
self._attempt_to_invalidate_cache("get_sliding_sync_rooms_for_user", None)
|
||||
self._attempt_to_invalidate_cache("did_forget", None)
|
||||
self._attempt_to_invalidate_cache("get_forgotten_rooms_for_user", None)
|
||||
self._attempt_to_invalidate_cache("_get_membership_from_event_id", None)
|
||||
|
||||
537
synapse/storage/databases/main/delayed_events.py
Normal file
537
synapse/storage/databases/main/delayed_events.py
Normal file
@@ -0,0 +1,537 @@
|
||||
#
|
||||
# This file is licensed under the Affero General Public License (AGPL) version 3.
|
||||
#
|
||||
# Copyright (C) 2024 New Vector, Ltd
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU Affero General Public License as
|
||||
# published by the Free Software Foundation, either version 3 of the
|
||||
# License, or (at your option) any later version.
|
||||
#
|
||||
# See the GNU Affero General Public License for more details:
|
||||
# <https://www.gnu.org/licenses/agpl-3.0.html>.
|
||||
#
|
||||
|
||||
import logging
|
||||
from typing import List, NewType, Optional, Tuple
|
||||
|
||||
import attr
|
||||
|
||||
from synapse.api.errors import NotFoundError
|
||||
from synapse.storage._base import SQLBaseStore, db_to_json
|
||||
from synapse.storage.database import LoggingTransaction, StoreError
|
||||
from synapse.storage.engines import PostgresEngine
|
||||
from synapse.types import JsonDict, RoomID
|
||||
from synapse.util import json_encoder, stringutils as stringutils
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
DelayID = NewType("DelayID", str)
|
||||
UserLocalpart = NewType("UserLocalpart", str)
|
||||
DeviceID = NewType("DeviceID", str)
|
||||
EventType = NewType("EventType", str)
|
||||
StateKey = NewType("StateKey", str)
|
||||
|
||||
Delay = NewType("Delay", int)
|
||||
Timestamp = NewType("Timestamp", int)
|
||||
|
||||
|
||||
@attr.s(slots=True, frozen=True, auto_attribs=True)
|
||||
class EventDetails:
|
||||
room_id: RoomID
|
||||
type: EventType
|
||||
state_key: Optional[StateKey]
|
||||
origin_server_ts: Optional[Timestamp]
|
||||
content: JsonDict
|
||||
device_id: Optional[DeviceID]
|
||||
|
||||
|
||||
@attr.s(slots=True, frozen=True, auto_attribs=True)
|
||||
class DelayedEventDetails(EventDetails):
|
||||
delay_id: DelayID
|
||||
user_localpart: UserLocalpart
|
||||
|
||||
|
||||
class DelayedEventsStore(SQLBaseStore):
|
||||
async def get_delayed_events_stream_pos(self) -> int:
|
||||
"""
|
||||
Gets the stream position of the background process to watch for state events
|
||||
that target the same piece of state as any pending delayed events.
|
||||
"""
|
||||
return await self.db_pool.simple_select_one_onecol(
|
||||
table="delayed_events_stream_pos",
|
||||
keyvalues={},
|
||||
retcol="stream_id",
|
||||
desc="get_delayed_events_stream_pos",
|
||||
)
|
||||
|
||||
async def update_delayed_events_stream_pos(self, stream_id: Optional[int]) -> None:
|
||||
"""
|
||||
Updates the stream position of the background process to watch for state events
|
||||
that target the same piece of state as any pending delayed events.
|
||||
|
||||
Must only be used by the worker running the background process.
|
||||
"""
|
||||
await self.db_pool.simple_update_one(
|
||||
table="delayed_events_stream_pos",
|
||||
keyvalues={},
|
||||
updatevalues={"stream_id": stream_id},
|
||||
desc="update_delayed_events_stream_pos",
|
||||
)
|
||||
|
||||
async def add_delayed_event(
|
||||
self,
|
||||
*,
|
||||
user_localpart: str,
|
||||
device_id: Optional[str],
|
||||
creation_ts: Timestamp,
|
||||
room_id: str,
|
||||
event_type: str,
|
||||
state_key: Optional[str],
|
||||
origin_server_ts: Optional[int],
|
||||
content: JsonDict,
|
||||
delay: int,
|
||||
) -> Tuple[DelayID, Timestamp]:
|
||||
"""
|
||||
Inserts a new delayed event in the DB.
|
||||
|
||||
Returns: The generated ID assigned to the added delayed event,
|
||||
and the send time of the next delayed event to be sent,
|
||||
which is either the event just added or one added earlier.
|
||||
"""
|
||||
delay_id = _generate_delay_id()
|
||||
send_ts = Timestamp(creation_ts + delay)
|
||||
|
||||
def add_delayed_event_txn(txn: LoggingTransaction) -> Timestamp:
|
||||
self.db_pool.simple_insert_txn(
|
||||
txn,
|
||||
table="delayed_events",
|
||||
values={
|
||||
"delay_id": delay_id,
|
||||
"user_localpart": user_localpart,
|
||||
"device_id": device_id,
|
||||
"delay": delay,
|
||||
"send_ts": send_ts,
|
||||
"room_id": room_id,
|
||||
"event_type": event_type,
|
||||
"state_key": state_key,
|
||||
"origin_server_ts": origin_server_ts,
|
||||
"content": json_encoder.encode(content),
|
||||
},
|
||||
)
|
||||
|
||||
next_send_ts = self._get_next_delayed_event_send_ts_txn(txn)
|
||||
assert next_send_ts is not None
|
||||
return next_send_ts
|
||||
|
||||
next_send_ts = await self.db_pool.runInteraction(
|
||||
"add_delayed_event", add_delayed_event_txn
|
||||
)
|
||||
|
||||
return delay_id, next_send_ts
|
||||
|
||||
async def restart_delayed_event(
|
||||
self,
|
||||
*,
|
||||
delay_id: str,
|
||||
user_localpart: str,
|
||||
current_ts: Timestamp,
|
||||
) -> Timestamp:
|
||||
"""
|
||||
Restarts the send time of the matching delayed event,
|
||||
as long as it hasn't already been marked for processing.
|
||||
|
||||
Args:
|
||||
delay_id: The ID of the delayed event to restart.
|
||||
user_localpart: The localpart of the delayed event's owner.
|
||||
current_ts: The current time, which will be used to calculate the new send time.
|
||||
|
||||
Returns: The send time of the next delayed event to be sent,
|
||||
which is either the event just restarted, or another one
|
||||
with an earlier send time than the restarted one's new send time.
|
||||
|
||||
Raises:
|
||||
NotFoundError: if there is no matching delayed event.
|
||||
"""
|
||||
|
||||
def restart_delayed_event_txn(
|
||||
txn: LoggingTransaction,
|
||||
) -> Timestamp:
|
||||
txn.execute(
|
||||
"""
|
||||
UPDATE delayed_events
|
||||
SET send_ts = ? + delay
|
||||
WHERE delay_id = ? AND user_localpart = ?
|
||||
AND NOT is_processed
|
||||
""",
|
||||
(
|
||||
current_ts,
|
||||
delay_id,
|
||||
user_localpart,
|
||||
),
|
||||
)
|
||||
if txn.rowcount == 0:
|
||||
raise NotFoundError("Delayed event not found")
|
||||
|
||||
next_send_ts = self._get_next_delayed_event_send_ts_txn(txn)
|
||||
assert next_send_ts is not None
|
||||
return next_send_ts
|
||||
|
||||
return await self.db_pool.runInteraction(
|
||||
"restart_delayed_event", restart_delayed_event_txn
|
||||
)
|
||||
|
||||
async def get_all_delayed_events_for_user(
|
||||
self,
|
||||
user_localpart: str,
|
||||
) -> List[JsonDict]:
|
||||
"""Returns all pending delayed events owned by the given user."""
|
||||
# TODO: Support Pagination stream API ("next_batch" field)
|
||||
rows = await self.db_pool.execute(
|
||||
"get_all_delayed_events_for_user",
|
||||
"""
|
||||
SELECT
|
||||
delay_id,
|
||||
room_id,
|
||||
event_type,
|
||||
state_key,
|
||||
delay,
|
||||
send_ts,
|
||||
content
|
||||
FROM delayed_events
|
||||
WHERE user_localpart = ? AND NOT is_processed
|
||||
ORDER BY send_ts
|
||||
""",
|
||||
user_localpart,
|
||||
)
|
||||
return [
|
||||
{
|
||||
"delay_id": DelayID(row[0]),
|
||||
"room_id": str(RoomID.from_string(row[1])),
|
||||
"type": EventType(row[2]),
|
||||
**({"state_key": StateKey(row[3])} if row[3] is not None else {}),
|
||||
"delay": Delay(row[4]),
|
||||
"running_since": Timestamp(row[5] - row[4]),
|
||||
"content": db_to_json(row[6]),
|
||||
}
|
||||
for row in rows
|
||||
]
|
||||
|
||||
async def process_timeout_delayed_events(
|
||||
self, current_ts: Timestamp
|
||||
) -> Tuple[
|
||||
List[DelayedEventDetails],
|
||||
Optional[Timestamp],
|
||||
]:
|
||||
"""
|
||||
Marks for processing all delayed events that should have been sent prior to the provided time
|
||||
that haven't already been marked as such.
|
||||
|
||||
Returns: The details of all newly-processed delayed events,
|
||||
and the send time of the next delayed event to be sent, if any.
|
||||
"""
|
||||
|
||||
def process_timeout_delayed_events_txn(
|
||||
txn: LoggingTransaction,
|
||||
) -> Tuple[
|
||||
List[DelayedEventDetails],
|
||||
Optional[Timestamp],
|
||||
]:
|
||||
sql_cols = ", ".join(
|
||||
(
|
||||
"delay_id",
|
||||
"user_localpart",
|
||||
"room_id",
|
||||
"event_type",
|
||||
"state_key",
|
||||
"origin_server_ts",
|
||||
"send_ts",
|
||||
"content",
|
||||
"device_id",
|
||||
)
|
||||
)
|
||||
sql_update = "UPDATE delayed_events SET is_processed = TRUE"
|
||||
sql_where = "WHERE send_ts <= ? AND NOT is_processed"
|
||||
sql_args = (current_ts,)
|
||||
sql_order = "ORDER BY send_ts"
|
||||
if isinstance(self.database_engine, PostgresEngine):
|
||||
# Do this only in Postgres because:
|
||||
# - SQLite's RETURNING emits rows in an arbitrary order
|
||||
# - https://www.sqlite.org/lang_returning.html#limitations_and_caveats
|
||||
# - SQLite does not support data-modifying statements in a WITH clause
|
||||
# - https://www.sqlite.org/lang_with.html
|
||||
# - https://www.postgresql.org/docs/current/queries-with.html#QUERIES-WITH-MODIFYING
|
||||
txn.execute(
|
||||
f"""
|
||||
WITH events_to_send AS (
|
||||
{sql_update} {sql_where} RETURNING *
|
||||
) SELECT {sql_cols} FROM events_to_send {sql_order}
|
||||
""",
|
||||
sql_args,
|
||||
)
|
||||
rows = txn.fetchall()
|
||||
else:
|
||||
txn.execute(
|
||||
f"SELECT {sql_cols} FROM delayed_events {sql_where} {sql_order}",
|
||||
sql_args,
|
||||
)
|
||||
rows = txn.fetchall()
|
||||
txn.execute(f"{sql_update} {sql_where}", sql_args)
|
||||
assert txn.rowcount == len(rows)
|
||||
|
||||
events = [
|
||||
DelayedEventDetails(
|
||||
RoomID.from_string(row[2]),
|
||||
EventType(row[3]),
|
||||
StateKey(row[4]) if row[4] is not None else None,
|
||||
# If no custom_origin_ts is set, use send_ts as the event's timestamp
|
||||
Timestamp(row[5] if row[5] is not None else row[6]),
|
||||
db_to_json(row[7]),
|
||||
DeviceID(row[8]) if row[8] is not None else None,
|
||||
DelayID(row[0]),
|
||||
UserLocalpart(row[1]),
|
||||
)
|
||||
for row in rows
|
||||
]
|
||||
next_send_ts = self._get_next_delayed_event_send_ts_txn(txn)
|
||||
return events, next_send_ts
|
||||
|
||||
return await self.db_pool.runInteraction(
|
||||
"process_timeout_delayed_events", process_timeout_delayed_events_txn
|
||||
)
|
||||
|
||||
async def process_target_delayed_event(
|
||||
self,
|
||||
*,
|
||||
delay_id: str,
|
||||
user_localpart: str,
|
||||
) -> Tuple[
|
||||
EventDetails,
|
||||
Optional[Timestamp],
|
||||
]:
|
||||
"""
|
||||
Marks for processing the matching delayed event, regardless of its timeout time,
|
||||
as long as it has not already been marked as such.
|
||||
|
||||
Args:
|
||||
delay_id: The ID of the delayed event to restart.
|
||||
user_localpart: The localpart of the delayed event's owner.
|
||||
|
||||
Returns: The details of the matching delayed event,
|
||||
and the send time of the next delayed event to be sent, if any.
|
||||
|
||||
Raises:
|
||||
NotFoundError: if there is no matching delayed event.
|
||||
"""
|
||||
|
||||
def process_target_delayed_event_txn(
|
||||
txn: LoggingTransaction,
|
||||
) -> Tuple[
|
||||
EventDetails,
|
||||
Optional[Timestamp],
|
||||
]:
|
||||
sql_cols = ", ".join(
|
||||
(
|
||||
"room_id",
|
||||
"event_type",
|
||||
"state_key",
|
||||
"origin_server_ts",
|
||||
"content",
|
||||
"device_id",
|
||||
)
|
||||
)
|
||||
sql_update = "UPDATE delayed_events SET is_processed = TRUE"
|
||||
sql_where = "WHERE delay_id = ? AND user_localpart = ? AND NOT is_processed"
|
||||
sql_args = (delay_id, user_localpart)
|
||||
txn.execute(
|
||||
(
|
||||
f"{sql_update} {sql_where} RETURNING {sql_cols}"
|
||||
if self.database_engine.supports_returning
|
||||
else f"SELECT {sql_cols} FROM delayed_events {sql_where}"
|
||||
),
|
||||
sql_args,
|
||||
)
|
||||
row = txn.fetchone()
|
||||
if row is None:
|
||||
raise NotFoundError("Delayed event not found")
|
||||
elif not self.database_engine.supports_returning:
|
||||
txn.execute(f"{sql_update} {sql_where}", sql_args)
|
||||
assert txn.rowcount == 1
|
||||
|
||||
event = EventDetails(
|
||||
RoomID.from_string(row[0]),
|
||||
EventType(row[1]),
|
||||
StateKey(row[2]) if row[2] is not None else None,
|
||||
Timestamp(row[3]) if row[3] is not None else None,
|
||||
db_to_json(row[4]),
|
||||
DeviceID(row[5]) if row[5] is not None else None,
|
||||
)
|
||||
|
||||
return event, self._get_next_delayed_event_send_ts_txn(txn)
|
||||
|
||||
return await self.db_pool.runInteraction(
|
||||
"process_target_delayed_event", process_target_delayed_event_txn
|
||||
)
|
||||
|
||||
async def cancel_delayed_event(
|
||||
self,
|
||||
*,
|
||||
delay_id: str,
|
||||
user_localpart: str,
|
||||
) -> Optional[Timestamp]:
|
||||
"""
|
||||
Cancels the matching delayed event, i.e. remove it as long as it hasn't been processed.
|
||||
|
||||
Args:
|
||||
delay_id: The ID of the delayed event to restart.
|
||||
user_localpart: The localpart of the delayed event's owner.
|
||||
|
||||
Returns: The send time of the next delayed event to be sent, if any.
|
||||
|
||||
Raises:
|
||||
NotFoundError: if there is no matching delayed event.
|
||||
"""
|
||||
|
||||
def cancel_delayed_event_txn(
|
||||
txn: LoggingTransaction,
|
||||
) -> Optional[Timestamp]:
|
||||
try:
|
||||
self.db_pool.simple_delete_one_txn(
|
||||
txn,
|
||||
table="delayed_events",
|
||||
keyvalues={
|
||||
"delay_id": delay_id,
|
||||
"user_localpart": user_localpart,
|
||||
"is_processed": False,
|
||||
},
|
||||
)
|
||||
except StoreError:
|
||||
if txn.rowcount == 0:
|
||||
raise NotFoundError("Delayed event not found")
|
||||
else:
|
||||
raise
|
||||
|
||||
return self._get_next_delayed_event_send_ts_txn(txn)
|
||||
|
||||
return await self.db_pool.runInteraction(
|
||||
"cancel_delayed_event", cancel_delayed_event_txn
|
||||
)
|
||||
|
||||
async def cancel_delayed_state_events(
|
||||
self,
|
||||
*,
|
||||
room_id: str,
|
||||
event_type: str,
|
||||
state_key: str,
|
||||
) -> Optional[Timestamp]:
|
||||
"""
|
||||
Cancels all matching delayed state events, i.e. remove them as long as they haven't been processed.
|
||||
|
||||
Returns: The send time of the next delayed event to be sent, if any.
|
||||
"""
|
||||
|
||||
def cancel_delayed_state_events_txn(
|
||||
txn: LoggingTransaction,
|
||||
) -> Optional[Timestamp]:
|
||||
self.db_pool.simple_delete_txn(
|
||||
txn,
|
||||
table="delayed_events",
|
||||
keyvalues={
|
||||
"room_id": room_id,
|
||||
"event_type": event_type,
|
||||
"state_key": state_key,
|
||||
"is_processed": False,
|
||||
},
|
||||
)
|
||||
return self._get_next_delayed_event_send_ts_txn(txn)
|
||||
|
||||
return await self.db_pool.runInteraction(
|
||||
"cancel_delayed_state_events", cancel_delayed_state_events_txn
|
||||
)
|
||||
|
||||
async def delete_processed_delayed_event(
|
||||
self,
|
||||
delay_id: DelayID,
|
||||
user_localpart: UserLocalpart,
|
||||
) -> None:
|
||||
"""
|
||||
Delete the matching delayed event, as long as it has been marked as processed.
|
||||
|
||||
Throws:
|
||||
StoreError: if there is no matching delayed event, or if it has not yet been processed.
|
||||
"""
|
||||
return await self.db_pool.simple_delete_one(
|
||||
table="delayed_events",
|
||||
keyvalues={
|
||||
"delay_id": delay_id,
|
||||
"user_localpart": user_localpart,
|
||||
"is_processed": True,
|
||||
},
|
||||
desc="delete_processed_delayed_event",
|
||||
)
|
||||
|
||||
async def delete_processed_delayed_state_events(
|
||||
self,
|
||||
*,
|
||||
room_id: str,
|
||||
event_type: str,
|
||||
state_key: str,
|
||||
) -> None:
|
||||
"""
|
||||
Delete the matching delayed state events that have been marked as processed.
|
||||
"""
|
||||
await self.db_pool.simple_delete(
|
||||
table="delayed_events",
|
||||
keyvalues={
|
||||
"room_id": room_id,
|
||||
"event_type": event_type,
|
||||
"state_key": state_key,
|
||||
"is_processed": True,
|
||||
},
|
||||
desc="delete_processed_delayed_state_events",
|
||||
)
|
||||
|
||||
async def unprocess_delayed_events(self) -> None:
|
||||
"""
|
||||
Unmark all delayed events for processing.
|
||||
"""
|
||||
await self.db_pool.simple_update(
|
||||
table="delayed_events",
|
||||
keyvalues={"is_processed": True},
|
||||
updatevalues={"is_processed": False},
|
||||
desc="unprocess_delayed_events",
|
||||
)
|
||||
|
||||
async def get_next_delayed_event_send_ts(self) -> Optional[Timestamp]:
|
||||
"""
|
||||
Returns the send time of the next delayed event to be sent, if any.
|
||||
"""
|
||||
return await self.db_pool.runInteraction(
|
||||
"get_next_delayed_event_send_ts",
|
||||
self._get_next_delayed_event_send_ts_txn,
|
||||
db_autocommit=True,
|
||||
)
|
||||
|
||||
def _get_next_delayed_event_send_ts_txn(
|
||||
self, txn: LoggingTransaction
|
||||
) -> Optional[Timestamp]:
|
||||
result = self.db_pool.simple_select_one_onecol_txn(
|
||||
txn,
|
||||
table="delayed_events",
|
||||
keyvalues={"is_processed": False},
|
||||
retcol="MIN(send_ts)",
|
||||
allow_none=True,
|
||||
)
|
||||
return Timestamp(result) if result is not None else None
|
||||
|
||||
|
||||
def _generate_delay_id() -> DelayID:
|
||||
"""Generates an opaque string, for use as a delay ID"""
|
||||
|
||||
# We use the following format for delay IDs:
|
||||
# syd_<random string>
|
||||
# They are scoped to user localparts, so it is possible for
|
||||
# the same ID to exist for multiple users.
|
||||
|
||||
return DelayID(f"syd_{stringutils.random_string(20)}")
|
||||
@@ -1863,10 +1863,10 @@ class PersistEventsStore:
|
||||
txn.execute_batch(
|
||||
f"""
|
||||
INSERT INTO sliding_sync_membership_snapshots
|
||||
(room_id, user_id, sender, membership_event_id, membership, event_stream_ordering, event_instance_name
|
||||
(room_id, user_id, sender, membership_event_id, membership, forgotten, event_stream_ordering, event_instance_name
|
||||
{("," + ", ".join(sliding_sync_snapshot_keys)) if sliding_sync_snapshot_keys else ""})
|
||||
VALUES (
|
||||
?, ?, ?, ?, ?,
|
||||
?, ?, ?, ?, ?, ?,
|
||||
(SELECT stream_ordering FROM events WHERE event_id = ?),
|
||||
(SELECT COALESCE(instance_name, 'master') FROM events WHERE event_id = ?)
|
||||
{("," + ", ".join("?" for _ in sliding_sync_snapshot_values)) if sliding_sync_snapshot_values else ""}
|
||||
@@ -1876,6 +1876,7 @@ class PersistEventsStore:
|
||||
sender = EXCLUDED.sender,
|
||||
membership_event_id = EXCLUDED.membership_event_id,
|
||||
membership = EXCLUDED.membership,
|
||||
forgotten = EXCLUDED.forgotten,
|
||||
event_stream_ordering = EXCLUDED.event_stream_ordering
|
||||
{("," + ", ".join(f"{key} = EXCLUDED.{key}" for key in sliding_sync_snapshot_keys)) if sliding_sync_snapshot_keys else ""}
|
||||
""",
|
||||
@@ -1886,6 +1887,9 @@ class PersistEventsStore:
|
||||
membership_info.sender,
|
||||
membership_info.membership_event_id,
|
||||
membership_info.membership,
|
||||
# Since this is a new membership, it isn't forgotten anymore (which
|
||||
# matches how Synapse currently thinks about the forgotten status)
|
||||
0,
|
||||
# XXX: We do not use `membership_info.membership_event_stream_ordering` here
|
||||
# because it is an unreliable value. See XXX note above.
|
||||
membership_info.membership_event_id,
|
||||
@@ -1980,7 +1984,12 @@ class PersistEventsStore:
|
||||
if state_key == (EventTypes.Create, ""):
|
||||
room_type = event.content.get(EventContentFields.ROOM_TYPE)
|
||||
# Scrutinize JSON values
|
||||
if room_type is None or isinstance(room_type, str):
|
||||
if room_type is None or (
|
||||
isinstance(room_type, str)
|
||||
# We ignore values with null bytes as Postgres doesn't allow them in
|
||||
# text columns.
|
||||
and "\0" not in room_type
|
||||
):
|
||||
sliding_sync_insert_map["room_type"] = room_type
|
||||
elif state_key == (EventTypes.RoomEncryption, ""):
|
||||
encryption_algorithm = event.content.get(
|
||||
@@ -1990,15 +1999,26 @@ class PersistEventsStore:
|
||||
sliding_sync_insert_map["is_encrypted"] = is_encrypted
|
||||
elif state_key == (EventTypes.Name, ""):
|
||||
room_name = event.content.get(EventContentFields.ROOM_NAME)
|
||||
# Scrutinize JSON values
|
||||
if room_name is None or isinstance(room_name, str):
|
||||
# Scrutinize JSON values. We ignore values with nulls as
|
||||
# postgres doesn't allow null bytes in text columns.
|
||||
if room_name is None or (
|
||||
isinstance(room_name, str)
|
||||
# We ignore values with null bytes as Postgres doesn't allow them in
|
||||
# text columns.
|
||||
and "\0" not in room_name
|
||||
):
|
||||
sliding_sync_insert_map["room_name"] = room_name
|
||||
elif state_key == (EventTypes.Tombstone, ""):
|
||||
successor_room_id = event.content.get(
|
||||
EventContentFields.TOMBSTONE_SUCCESSOR_ROOM
|
||||
)
|
||||
# Scrutinize JSON values
|
||||
if successor_room_id is None or isinstance(successor_room_id, str):
|
||||
if successor_room_id is None or (
|
||||
isinstance(successor_room_id, str)
|
||||
# We ignore values with null bytes as Postgres doesn't allow them in
|
||||
# text columns.
|
||||
and "\0" not in successor_room_id
|
||||
):
|
||||
sliding_sync_insert_map["tombstone_successor_room_id"] = (
|
||||
successor_room_id
|
||||
)
|
||||
@@ -2081,6 +2101,21 @@ class PersistEventsStore:
|
||||
else None
|
||||
)
|
||||
|
||||
# Check for null bytes in the room name and type. We have to
|
||||
# ignore values with null bytes as Postgres doesn't allow them
|
||||
# in text columns.
|
||||
if (
|
||||
sliding_sync_insert_map["room_name"] is not None
|
||||
and "\0" in sliding_sync_insert_map["room_name"]
|
||||
):
|
||||
sliding_sync_insert_map.pop("room_name")
|
||||
|
||||
if (
|
||||
sliding_sync_insert_map["room_type"] is not None
|
||||
and "\0" in sliding_sync_insert_map["room_type"]
|
||||
):
|
||||
sliding_sync_insert_map.pop("room_type")
|
||||
|
||||
# Find the tombstone_successor_room_id
|
||||
# Note: This isn't one of the stripped state events according to the spec
|
||||
# but seems like there is no reason not to support this kind of thing.
|
||||
@@ -2095,6 +2130,12 @@ class PersistEventsStore:
|
||||
else None
|
||||
)
|
||||
|
||||
if (
|
||||
sliding_sync_insert_map["tombstone_successor_room_id"] is not None
|
||||
and "\0" in sliding_sync_insert_map["tombstone_successor_room_id"]
|
||||
):
|
||||
sliding_sync_insert_map.pop("tombstone_successor_room_id")
|
||||
|
||||
else:
|
||||
# No stripped state provided
|
||||
sliding_sync_insert_map["has_known_state"] = False
|
||||
@@ -2864,6 +2905,9 @@ class PersistEventsStore:
|
||||
"sender": event.sender,
|
||||
"membership_event_id": event.event_id,
|
||||
"membership": event.membership,
|
||||
# Since this is a new membership, it isn't forgotten anymore (which
|
||||
# matches how Synapse currently thinks about the forgotten status)
|
||||
"forgotten": 0,
|
||||
"event_stream_ordering": event.internal_metadata.stream_ordering,
|
||||
"event_instance_name": event.internal_metadata.instance_name,
|
||||
}
|
||||
|
||||
@@ -47,6 +47,7 @@ from synapse.storage.databases.main.events_worker import (
|
||||
)
|
||||
from synapse.storage.databases.main.state_deltas import StateDeltasStore
|
||||
from synapse.storage.databases.main.stream import StreamWorkerStore
|
||||
from synapse.storage.engines import PostgresEngine
|
||||
from synapse.storage.types import Cursor
|
||||
from synapse.types import JsonDict, RoomStreamToken, StateMap, StrCollection
|
||||
from synapse.types.handlers import SLIDING_SYNC_DEFAULT_BUMP_EVENT_TYPES
|
||||
@@ -303,6 +304,12 @@ class EventsBackgroundUpdatesStore(StreamWorkerStore, StateDeltasStore, SQLBaseS
|
||||
_BackgroundUpdates.SLIDING_SYNC_MEMBERSHIP_SNAPSHOTS_BG_UPDATE,
|
||||
self._sliding_sync_membership_snapshots_bg_update,
|
||||
)
|
||||
# Add a background update to fix data integrity issue in the
|
||||
# `sliding_sync_membership_snapshots` -> `forgotten` column
|
||||
self.db_pool.updates.register_background_update_handler(
|
||||
_BackgroundUpdates.SLIDING_SYNC_MEMBERSHIP_SNAPSHOTS_FIX_FORGOTTEN_COLUMN_BG_UPDATE,
|
||||
self._sliding_sync_membership_snapshots_fix_forgotten_column_bg_update,
|
||||
)
|
||||
|
||||
# We want this to run on the main database at startup before we start processing
|
||||
# events.
|
||||
@@ -1850,9 +1857,29 @@ class EventsBackgroundUpdatesStore(StreamWorkerStore, StateDeltasStore, SQLBaseS
|
||||
def _find_memberships_to_update_txn(
|
||||
txn: LoggingTransaction,
|
||||
) -> List[
|
||||
Tuple[str, Optional[str], str, str, str, str, int, Optional[str], bool]
|
||||
Tuple[
|
||||
str,
|
||||
Optional[str],
|
||||
Optional[str],
|
||||
str,
|
||||
str,
|
||||
str,
|
||||
str,
|
||||
int,
|
||||
Optional[str],
|
||||
bool,
|
||||
]
|
||||
]:
|
||||
# Fetch the set of event IDs that we want to update
|
||||
#
|
||||
# We skip over rows which we've already handled, i.e. have a
|
||||
# matching row in `sliding_sync_membership_snapshots` with the same
|
||||
# room, user and event ID.
|
||||
#
|
||||
# We also ignore rooms that the user has left themselves (i.e. not
|
||||
# kicked). This is to avoid having to port lots of old rooms that we
|
||||
# will never send down sliding sync (as we exclude such rooms from
|
||||
# initial syncs).
|
||||
|
||||
if initial_phase:
|
||||
# There are some old out-of-band memberships (before
|
||||
@@ -1865,6 +1892,7 @@ class EventsBackgroundUpdatesStore(StreamWorkerStore, StateDeltasStore, SQLBaseS
|
||||
SELECT
|
||||
c.room_id,
|
||||
r.room_id,
|
||||
r.room_version,
|
||||
c.user_id,
|
||||
e.sender,
|
||||
c.event_id,
|
||||
@@ -1873,9 +1901,11 @@ class EventsBackgroundUpdatesStore(StreamWorkerStore, StateDeltasStore, SQLBaseS
|
||||
e.instance_name,
|
||||
e.outlier
|
||||
FROM local_current_membership AS c
|
||||
LEFT JOIN sliding_sync_membership_snapshots AS m USING (room_id, user_id)
|
||||
INNER JOIN events AS e USING (event_id)
|
||||
LEFT JOIN rooms AS r ON (c.room_id = r.room_id)
|
||||
WHERE (c.room_id, c.user_id) > (?, ?)
|
||||
AND (m.user_id IS NULL OR c.event_id != m.membership_event_id)
|
||||
ORDER BY c.room_id ASC, c.user_id ASC
|
||||
LIMIT ?
|
||||
""",
|
||||
@@ -1895,7 +1925,8 @@ class EventsBackgroundUpdatesStore(StreamWorkerStore, StateDeltasStore, SQLBaseS
|
||||
"""
|
||||
SELECT
|
||||
c.room_id,
|
||||
c.room_id,
|
||||
r.room_id,
|
||||
r.room_version,
|
||||
c.user_id,
|
||||
e.sender,
|
||||
c.event_id,
|
||||
@@ -1904,9 +1935,12 @@ class EventsBackgroundUpdatesStore(StreamWorkerStore, StateDeltasStore, SQLBaseS
|
||||
e.instance_name,
|
||||
e.outlier
|
||||
FROM local_current_membership AS c
|
||||
LEFT JOIN sliding_sync_membership_snapshots AS m USING (room_id, user_id)
|
||||
INNER JOIN events AS e USING (event_id)
|
||||
WHERE event_stream_ordering > ?
|
||||
ORDER BY event_stream_ordering ASC
|
||||
LEFT JOIN rooms AS r ON (c.room_id = r.room_id)
|
||||
WHERE c.event_stream_ordering > ?
|
||||
AND (m.user_id IS NULL OR c.event_id != m.membership_event_id)
|
||||
ORDER BY c.event_stream_ordering ASC
|
||||
LIMIT ?
|
||||
""",
|
||||
(last_event_stream_ordering, batch_size),
|
||||
@@ -1917,7 +1951,16 @@ class EventsBackgroundUpdatesStore(StreamWorkerStore, StateDeltasStore, SQLBaseS
|
||||
memberships_to_update_rows = cast(
|
||||
List[
|
||||
Tuple[
|
||||
str, Optional[str], str, str, str, str, int, Optional[str], bool
|
||||
str,
|
||||
Optional[str],
|
||||
Optional[str],
|
||||
str,
|
||||
str,
|
||||
str,
|
||||
str,
|
||||
int,
|
||||
Optional[str],
|
||||
bool,
|
||||
]
|
||||
],
|
||||
txn.fetchall(),
|
||||
@@ -1950,7 +1993,7 @@ class EventsBackgroundUpdatesStore(StreamWorkerStore, StateDeltasStore, SQLBaseS
|
||||
|
||||
def _find_previous_invite_or_knock_membership_txn(
|
||||
txn: LoggingTransaction, room_id: str, user_id: str, event_id: str
|
||||
) -> Tuple[str, str]:
|
||||
) -> Optional[Tuple[str, str]]:
|
||||
# Find the previous invite/knock event before the leave event
|
||||
#
|
||||
# Here are some notes on how we landed on this query:
|
||||
@@ -2000,8 +2043,13 @@ class EventsBackgroundUpdatesStore(StreamWorkerStore, StateDeltasStore, SQLBaseS
|
||||
)
|
||||
row = txn.fetchone()
|
||||
|
||||
# We should see a corresponding previous invite/knock event
|
||||
assert row is not None
|
||||
if row is None:
|
||||
# Generally we should have an invite or knock event for leaves
|
||||
# that are outliers, however this may not always be the case
|
||||
# (e.g. a local user got kicked but the kick event got pulled in
|
||||
# as an outlier).
|
||||
return None
|
||||
|
||||
event_id, membership = row
|
||||
|
||||
return event_id, membership
|
||||
@@ -2016,6 +2064,7 @@ class EventsBackgroundUpdatesStore(StreamWorkerStore, StateDeltasStore, SQLBaseS
|
||||
for (
|
||||
room_id,
|
||||
room_id_from_rooms_table,
|
||||
room_version_id,
|
||||
user_id,
|
||||
sender,
|
||||
membership_event_id,
|
||||
@@ -2034,6 +2083,14 @@ class EventsBackgroundUpdatesStore(StreamWorkerStore, StateDeltasStore, SQLBaseS
|
||||
Membership.BAN,
|
||||
)
|
||||
|
||||
if (
|
||||
room_version_id is not None
|
||||
and room_version_id not in KNOWN_ROOM_VERSIONS
|
||||
):
|
||||
# Ignore rooms with unknown room versions (these were
|
||||
# experimental rooms, that we no longer support).
|
||||
continue
|
||||
|
||||
# There are some old out-of-band memberships (before
|
||||
# https://github.com/matrix-org/synapse/issues/6983) where we don't have the
|
||||
# corresponding room stored in the `rooms` table`. We have a `FOREIGN KEY`
|
||||
@@ -2121,14 +2178,17 @@ class EventsBackgroundUpdatesStore(StreamWorkerStore, StateDeltasStore, SQLBaseS
|
||||
# in the events table though. We'll just say that we don't
|
||||
# know the state for these rooms and continue on with our
|
||||
# day.
|
||||
sliding_sync_membership_snapshots_insert_map["has_known_state"] = (
|
||||
False
|
||||
)
|
||||
sliding_sync_membership_snapshots_insert_map = {
|
||||
"has_known_state": False,
|
||||
"room_type": None,
|
||||
"room_name": None,
|
||||
"is_encrypted": False,
|
||||
}
|
||||
elif membership in (Membership.INVITE, Membership.KNOCK) or (
|
||||
membership in (Membership.LEAVE, Membership.BAN) and is_outlier
|
||||
):
|
||||
invite_or_knock_event_id = membership_event_id
|
||||
invite_or_knock_membership = membership
|
||||
invite_or_knock_event_id = None
|
||||
invite_or_knock_membership = None
|
||||
|
||||
# If the event is an `out_of_band_membership` (special case of
|
||||
# `outlier`), we never had historical state so we have to pull from
|
||||
@@ -2137,35 +2197,55 @@ class EventsBackgroundUpdatesStore(StreamWorkerStore, StateDeltasStore, SQLBaseS
|
||||
# membership (i.e. the room shouldn't disappear if your using the
|
||||
# `is_encrypted` filter and you leave).
|
||||
if membership in (Membership.LEAVE, Membership.BAN) and is_outlier:
|
||||
(
|
||||
invite_or_knock_event_id,
|
||||
invite_or_knock_membership,
|
||||
) = await self.db_pool.runInteraction(
|
||||
previous_membership = await self.db_pool.runInteraction(
|
||||
"sliding_sync_membership_snapshots_bg_update._find_previous_invite_or_knock_membership_txn",
|
||||
_find_previous_invite_or_knock_membership_txn,
|
||||
room_id,
|
||||
user_id,
|
||||
membership_event_id,
|
||||
)
|
||||
if previous_membership is not None:
|
||||
(
|
||||
invite_or_knock_event_id,
|
||||
invite_or_knock_membership,
|
||||
) = previous_membership
|
||||
else:
|
||||
invite_or_knock_event_id = membership_event_id
|
||||
invite_or_knock_membership = membership
|
||||
|
||||
# Pull from the stripped state on the invite/knock event
|
||||
invite_or_knock_event = await self.get_event(invite_or_knock_event_id)
|
||||
|
||||
raw_stripped_state_events = None
|
||||
if invite_or_knock_membership == Membership.INVITE:
|
||||
invite_room_state = invite_or_knock_event.unsigned.get(
|
||||
"invite_room_state"
|
||||
if (
|
||||
invite_or_knock_event_id is not None
|
||||
and invite_or_knock_membership is not None
|
||||
):
|
||||
# Pull from the stripped state on the invite/knock event
|
||||
invite_or_knock_event = await self.get_event(
|
||||
invite_or_knock_event_id
|
||||
)
|
||||
raw_stripped_state_events = invite_room_state
|
||||
elif invite_or_knock_membership == Membership.KNOCK:
|
||||
knock_room_state = invite_or_knock_event.unsigned.get(
|
||||
"knock_room_state"
|
||||
)
|
||||
raw_stripped_state_events = knock_room_state
|
||||
|
||||
sliding_sync_membership_snapshots_insert_map = PersistEventsStore._get_sliding_sync_insert_values_from_stripped_state(
|
||||
raw_stripped_state_events
|
||||
)
|
||||
raw_stripped_state_events = None
|
||||
if invite_or_knock_membership == Membership.INVITE:
|
||||
invite_room_state = invite_or_knock_event.unsigned.get(
|
||||
"invite_room_state"
|
||||
)
|
||||
raw_stripped_state_events = invite_room_state
|
||||
elif invite_or_knock_membership == Membership.KNOCK:
|
||||
knock_room_state = invite_or_knock_event.unsigned.get(
|
||||
"knock_room_state"
|
||||
)
|
||||
raw_stripped_state_events = knock_room_state
|
||||
|
||||
sliding_sync_membership_snapshots_insert_map = PersistEventsStore._get_sliding_sync_insert_values_from_stripped_state(
|
||||
raw_stripped_state_events
|
||||
)
|
||||
else:
|
||||
# We couldn't find any state for the membership, so we just have to
|
||||
# leave it as empty.
|
||||
sliding_sync_membership_snapshots_insert_map = {
|
||||
"has_known_state": False,
|
||||
"room_type": None,
|
||||
"room_name": None,
|
||||
"is_encrypted": False,
|
||||
}
|
||||
|
||||
# We should have some insert values for each room, even if no
|
||||
# stripped state is on the event because we still want to record
|
||||
@@ -2284,19 +2364,42 @@ class EventsBackgroundUpdatesStore(StreamWorkerStore, StateDeltasStore, SQLBaseS
|
||||
)
|
||||
# We need to find the `forgotten` value during the transaction because
|
||||
# we can't risk inserting stale data.
|
||||
txn.execute(
|
||||
"""
|
||||
UPDATE sliding_sync_membership_snapshots
|
||||
SET
|
||||
forgotten = (SELECT forgotten FROM room_memberships WHERE event_id = ?)
|
||||
WHERE room_id = ? and user_id = ?
|
||||
""",
|
||||
(
|
||||
membership_event_id,
|
||||
room_id,
|
||||
user_id,
|
||||
),
|
||||
)
|
||||
if isinstance(txn.database_engine, PostgresEngine):
|
||||
txn.execute(
|
||||
"""
|
||||
UPDATE sliding_sync_membership_snapshots
|
||||
SET
|
||||
forgotten = m.forgotten
|
||||
FROM room_memberships AS m
|
||||
WHERE sliding_sync_membership_snapshots.room_id = ?
|
||||
AND sliding_sync_membership_snapshots.user_id = ?
|
||||
AND membership_event_id = ?
|
||||
AND membership_event_id = m.event_id
|
||||
AND m.event_id IS NOT NULL
|
||||
""",
|
||||
(
|
||||
room_id,
|
||||
user_id,
|
||||
membership_event_id,
|
||||
),
|
||||
)
|
||||
else:
|
||||
# SQLite doesn't support UPDATE FROM before 3.33.0, so we do
|
||||
# this via sub-selects.
|
||||
txn.execute(
|
||||
"""
|
||||
UPDATE sliding_sync_membership_snapshots
|
||||
SET
|
||||
forgotten = (SELECT forgotten FROM room_memberships WHERE event_id = ?)
|
||||
WHERE room_id = ? and user_id = ? AND membership_event_id = ?
|
||||
""",
|
||||
(
|
||||
membership_event_id,
|
||||
room_id,
|
||||
user_id,
|
||||
membership_event_id,
|
||||
),
|
||||
)
|
||||
|
||||
await self.db_pool.runInteraction(
|
||||
"sliding_sync_membership_snapshots_bg_update", _fill_table_txn
|
||||
@@ -2306,6 +2409,7 @@ class EventsBackgroundUpdatesStore(StreamWorkerStore, StateDeltasStore, SQLBaseS
|
||||
(
|
||||
room_id,
|
||||
_room_id_from_rooms_table,
|
||||
_room_version_id,
|
||||
user_id,
|
||||
_sender,
|
||||
_membership_event_id,
|
||||
@@ -2331,6 +2435,118 @@ class EventsBackgroundUpdatesStore(StreamWorkerStore, StateDeltasStore, SQLBaseS
|
||||
|
||||
return len(memberships_to_update_rows)
|
||||
|
||||
async def _sliding_sync_membership_snapshots_fix_forgotten_column_bg_update(
|
||||
self, progress: JsonDict, batch_size: int
|
||||
) -> int:
|
||||
"""
|
||||
Background update to update the `sliding_sync_membership_snapshots` ->
|
||||
`forgotten` column to be in sync with the `room_memberships` table.
|
||||
|
||||
Because of previously flawed code (now fixed); any room that someone has
|
||||
forgotten and subsequently re-joined or had any new membership on, we need to go
|
||||
and update the column to match the `room_memberships` table as it has fallen out
|
||||
of sync.
|
||||
"""
|
||||
last_event_stream_ordering = progress.get(
|
||||
"last_event_stream_ordering", -(1 << 31)
|
||||
)
|
||||
|
||||
def _txn(
|
||||
txn: LoggingTransaction,
|
||||
) -> int:
|
||||
"""
|
||||
Returns:
|
||||
The number of rows updated.
|
||||
"""
|
||||
|
||||
# To simplify things, we can just recheck any row in
|
||||
# `sliding_sync_membership_snapshots` with `forgotten=1`
|
||||
txn.execute(
|
||||
"""
|
||||
SELECT
|
||||
s.room_id,
|
||||
s.user_id,
|
||||
s.membership_event_id,
|
||||
s.event_stream_ordering,
|
||||
m.forgotten
|
||||
FROM sliding_sync_membership_snapshots AS s
|
||||
INNER JOIN room_memberships AS m ON (s.membership_event_id = m.event_id)
|
||||
WHERE s.event_stream_ordering > ?
|
||||
AND s.forgotten = 1
|
||||
ORDER BY s.event_stream_ordering ASC
|
||||
LIMIT ?
|
||||
""",
|
||||
(last_event_stream_ordering, batch_size),
|
||||
)
|
||||
|
||||
memberships_to_update_rows = cast(
|
||||
List[Tuple[str, str, str, int, int]],
|
||||
txn.fetchall(),
|
||||
)
|
||||
if not memberships_to_update_rows:
|
||||
return 0
|
||||
|
||||
# Assemble the values to update
|
||||
#
|
||||
# (room_id, user_id)
|
||||
key_values: List[Tuple[str, str]] = []
|
||||
# (forgotten,)
|
||||
value_values: List[Tuple[int]] = []
|
||||
for (
|
||||
room_id,
|
||||
user_id,
|
||||
_membership_event_id,
|
||||
_event_stream_ordering,
|
||||
forgotten,
|
||||
) in memberships_to_update_rows:
|
||||
key_values.append(
|
||||
(
|
||||
room_id,
|
||||
user_id,
|
||||
)
|
||||
)
|
||||
value_values.append((forgotten,))
|
||||
|
||||
# Update all of the rows in one go
|
||||
self.db_pool.simple_update_many_txn(
|
||||
txn,
|
||||
table="sliding_sync_membership_snapshots",
|
||||
key_names=("room_id", "user_id"),
|
||||
key_values=key_values,
|
||||
value_names=("forgotten",),
|
||||
value_values=value_values,
|
||||
)
|
||||
|
||||
# Update the progress
|
||||
(
|
||||
_room_id,
|
||||
_user_id,
|
||||
_membership_event_id,
|
||||
event_stream_ordering,
|
||||
_forgotten,
|
||||
) = memberships_to_update_rows[-1]
|
||||
self.db_pool.updates._background_update_progress_txn(
|
||||
txn,
|
||||
_BackgroundUpdates.SLIDING_SYNC_MEMBERSHIP_SNAPSHOTS_FIX_FORGOTTEN_COLUMN_BG_UPDATE,
|
||||
{
|
||||
"last_event_stream_ordering": event_stream_ordering,
|
||||
},
|
||||
)
|
||||
|
||||
return len(memberships_to_update_rows)
|
||||
|
||||
num_rows = await self.db_pool.runInteraction(
|
||||
"_sliding_sync_membership_snapshots_fix_forgotten_column_bg_update",
|
||||
_txn,
|
||||
)
|
||||
|
||||
if not num_rows:
|
||||
await self.db_pool.updates._end_background_update(
|
||||
_BackgroundUpdates.SLIDING_SYNC_MEMBERSHIP_SNAPSHOTS_FIX_FORGOTTEN_COLUMN_BG_UPDATE
|
||||
)
|
||||
|
||||
return num_rows
|
||||
|
||||
|
||||
def _resolve_stale_data_in_sliding_sync_tables(
|
||||
txn: LoggingTransaction,
|
||||
|
||||
@@ -61,7 +61,13 @@ from synapse.logging.context import (
|
||||
current_context,
|
||||
make_deferred_yieldable,
|
||||
)
|
||||
from synapse.logging.opentracing import start_active_span, tag_args, trace
|
||||
from synapse.logging.opentracing import (
|
||||
SynapseTags,
|
||||
set_tag,
|
||||
start_active_span,
|
||||
tag_args,
|
||||
trace,
|
||||
)
|
||||
from synapse.metrics.background_process_metrics import (
|
||||
run_as_background_process,
|
||||
wrap_as_background_process,
|
||||
@@ -525,6 +531,7 @@ class EventsWorkerStore(SQLBaseStore):
|
||||
|
||||
return event
|
||||
|
||||
@trace
|
||||
async def get_events(
|
||||
self,
|
||||
event_ids: Collection[str],
|
||||
@@ -556,6 +563,11 @@ class EventsWorkerStore(SQLBaseStore):
|
||||
Returns:
|
||||
A mapping from event_id to event.
|
||||
"""
|
||||
set_tag(
|
||||
SynapseTags.FUNC_ARG_PREFIX + "event_ids.length",
|
||||
str(len(event_ids)),
|
||||
)
|
||||
|
||||
events = await self.get_events_as_list(
|
||||
event_ids,
|
||||
redact_behaviour=redact_behaviour,
|
||||
@@ -603,6 +615,10 @@ class EventsWorkerStore(SQLBaseStore):
|
||||
Note that the returned list may be smaller than the list of event
|
||||
IDs if not all events could be fetched.
|
||||
"""
|
||||
set_tag(
|
||||
SynapseTags.FUNC_ARG_PREFIX + "event_ids.length",
|
||||
str(len(event_ids)),
|
||||
)
|
||||
|
||||
if not event_ids:
|
||||
return []
|
||||
@@ -723,10 +739,11 @@ class EventsWorkerStore(SQLBaseStore):
|
||||
|
||||
return events
|
||||
|
||||
@trace
|
||||
@cancellable
|
||||
async def get_unredacted_events_from_cache_or_db(
|
||||
self,
|
||||
event_ids: Iterable[str],
|
||||
event_ids: Collection[str],
|
||||
allow_rejected: bool = False,
|
||||
) -> Dict[str, EventCacheEntry]:
|
||||
"""Fetch a bunch of events from the cache or the database.
|
||||
@@ -748,6 +765,11 @@ class EventsWorkerStore(SQLBaseStore):
|
||||
Returns:
|
||||
map from event id to result
|
||||
"""
|
||||
set_tag(
|
||||
SynapseTags.FUNC_ARG_PREFIX + "event_ids.length",
|
||||
str(len(event_ids)),
|
||||
)
|
||||
|
||||
# Shortcut: check if we have any events in the *in memory* cache - this function
|
||||
# may be called repeatedly for the same event so at this point we cannot reach
|
||||
# out to any external cache for performance reasons. The external cache is
|
||||
@@ -936,7 +958,7 @@ class EventsWorkerStore(SQLBaseStore):
|
||||
events, update_metrics=update_metrics
|
||||
)
|
||||
|
||||
missing_event_ids = (e for e in events if e not in event_map)
|
||||
missing_event_ids = [e for e in events if e not in event_map]
|
||||
event_map.update(
|
||||
await self._get_events_from_external_cache(
|
||||
events=missing_event_ids,
|
||||
@@ -946,8 +968,9 @@ class EventsWorkerStore(SQLBaseStore):
|
||||
|
||||
return event_map
|
||||
|
||||
@trace
|
||||
async def _get_events_from_external_cache(
|
||||
self, events: Iterable[str], update_metrics: bool = True
|
||||
self, events: Collection[str], update_metrics: bool = True
|
||||
) -> Dict[str, EventCacheEntry]:
|
||||
"""Fetch events from any configured external cache.
|
||||
|
||||
@@ -957,6 +980,10 @@ class EventsWorkerStore(SQLBaseStore):
|
||||
events: list of event_ids to fetch
|
||||
update_metrics: Whether to update the cache hit ratio metrics
|
||||
"""
|
||||
set_tag(
|
||||
SynapseTags.FUNC_ARG_PREFIX + "events.length",
|
||||
str(len(events)),
|
||||
)
|
||||
event_map = {}
|
||||
|
||||
for event_id in events:
|
||||
@@ -1222,6 +1249,7 @@ class EventsWorkerStore(SQLBaseStore):
|
||||
with PreserveLoggingContext():
|
||||
self.hs.get_reactor().callFromThread(fire_errback, e)
|
||||
|
||||
@trace
|
||||
async def _get_events_from_db(
|
||||
self, event_ids: Collection[str]
|
||||
) -> Dict[str, EventCacheEntry]:
|
||||
@@ -1240,6 +1268,11 @@ class EventsWorkerStore(SQLBaseStore):
|
||||
map from event id to result. May return extra events which
|
||||
weren't asked for.
|
||||
"""
|
||||
set_tag(
|
||||
SynapseTags.FUNC_ARG_PREFIX + "event_ids.length",
|
||||
str(len(event_ids)),
|
||||
)
|
||||
|
||||
fetched_event_ids: Set[str] = set()
|
||||
fetched_events: Dict[str, _EventRow] = {}
|
||||
|
||||
@@ -2467,6 +2500,76 @@ class EventsWorkerStore(SQLBaseStore):
|
||||
|
||||
self.invalidate_get_event_cache_after_txn(txn, event_id)
|
||||
|
||||
async def get_events_sent_by_user_in_room(
|
||||
self, user_id: str, room_id: str, limit: int, filter: Optional[List[str]] = None
|
||||
) -> Optional[List[str]]:
|
||||
"""
|
||||
Get a list of event ids of events sent by the user in the specified room
|
||||
|
||||
Args:
|
||||
user_id: user ID to search against
|
||||
room_id: room ID of the room to search for events in
|
||||
filter: type of events to filter for
|
||||
limit: maximum number of event ids to return
|
||||
"""
|
||||
|
||||
def _get_events_by_user_in_room_txn(
|
||||
txn: LoggingTransaction,
|
||||
user_id: str,
|
||||
room_id: str,
|
||||
filter: Optional[List[str]],
|
||||
batch_size: int,
|
||||
offset: int,
|
||||
) -> Tuple[Optional[List[str]], int]:
|
||||
if filter:
|
||||
base_clause, args = make_in_list_sql_clause(
|
||||
txn.database_engine, "type", filter
|
||||
)
|
||||
clause = f"AND {base_clause}"
|
||||
parameters = (user_id, room_id, *args, batch_size, offset)
|
||||
else:
|
||||
clause = ""
|
||||
parameters = (user_id, room_id, batch_size, offset)
|
||||
|
||||
sql = f"""
|
||||
SELECT event_id FROM events
|
||||
WHERE sender = ? AND room_id = ?
|
||||
{clause}
|
||||
ORDER BY received_ts DESC
|
||||
LIMIT ?
|
||||
OFFSET ?
|
||||
"""
|
||||
txn.execute(sql, parameters)
|
||||
res = txn.fetchall()
|
||||
if res:
|
||||
events = [row[0] for row in res]
|
||||
else:
|
||||
events = None
|
||||
|
||||
return events, offset + batch_size
|
||||
|
||||
offset = 0
|
||||
batch_size = 100
|
||||
if batch_size > limit:
|
||||
batch_size = limit
|
||||
|
||||
selected_ids: List[str] = []
|
||||
while offset < limit:
|
||||
res, offset = await self.db_pool.runInteraction(
|
||||
"get_events_by_user",
|
||||
_get_events_by_user_in_room_txn,
|
||||
user_id,
|
||||
room_id,
|
||||
filter,
|
||||
batch_size,
|
||||
offset,
|
||||
)
|
||||
if res:
|
||||
selected_ids = selected_ids + res
|
||||
else:
|
||||
break
|
||||
return selected_ids
|
||||
|
||||
async def have_finished_sliding_sync_background_jobs(self) -> bool:
|
||||
"""Return if it's safe to use the sliding sync membership tables."""
|
||||
|
||||
|
||||
@@ -109,6 +109,7 @@ def _load_rules(
|
||||
msc3664_enabled=experimental_config.msc3664_enabled,
|
||||
msc3381_polls_enabled=experimental_config.msc3381_polls_enabled,
|
||||
msc4028_push_encrypted_events=experimental_config.msc4028_push_encrypted_events,
|
||||
msc4210_enabled=experimental_config.msc4210_enabled,
|
||||
)
|
||||
|
||||
return filtered_rules
|
||||
|
||||
@@ -41,6 +41,7 @@ import attr
|
||||
|
||||
from synapse.api.constants import EventTypes, Membership
|
||||
from synapse.api.errors import Codes, SynapseError
|
||||
from synapse.api.room_versions import KNOWN_ROOM_VERSIONS
|
||||
from synapse.logging.opentracing import trace
|
||||
from synapse.metrics import LaterGauge
|
||||
from synapse.metrics.background_process_metrics import wrap_as_background_process
|
||||
@@ -311,18 +312,10 @@ class RoomMemberWorkerStore(EventsWorkerStore, CacheInvalidationWorkerStore):
|
||||
# We do this all in one transaction to keep the cache small.
|
||||
# FIXME: get rid of this when we have room_stats
|
||||
|
||||
# Note, rejected events will have a null membership field, so
|
||||
# we we manually filter them out.
|
||||
sql = """
|
||||
SELECT count(*), membership FROM current_state_events
|
||||
WHERE type = 'm.room.member' AND room_id = ?
|
||||
AND membership IS NOT NULL
|
||||
GROUP BY membership
|
||||
"""
|
||||
counts = self._get_member_counts_txn(txn, room_id)
|
||||
|
||||
txn.execute(sql, (room_id,))
|
||||
res: Dict[str, MemberSummary] = {}
|
||||
for count, membership in txn:
|
||||
for membership, count in counts.items():
|
||||
res.setdefault(membership, MemberSummary([], count))
|
||||
|
||||
# Order by membership (joins -> invites -> leave (former insiders) ->
|
||||
@@ -368,6 +361,31 @@ class RoomMemberWorkerStore(EventsWorkerStore, CacheInvalidationWorkerStore):
|
||||
"get_room_summary", _get_room_summary_txn
|
||||
)
|
||||
|
||||
@cached()
|
||||
async def get_member_counts(self, room_id: str) -> Mapping[str, int]:
|
||||
"""Get a mapping of number of users by membership"""
|
||||
|
||||
return await self.db_pool.runInteraction(
|
||||
"get_member_counts", self._get_member_counts_txn, room_id
|
||||
)
|
||||
|
||||
def _get_member_counts_txn(
|
||||
self, txn: LoggingTransaction, room_id: str
|
||||
) -> Dict[str, int]:
|
||||
"""Get a mapping of number of users by membership"""
|
||||
|
||||
# Note, rejected events will have a null membership field, so
|
||||
# we we manually filter them out.
|
||||
sql = """
|
||||
SELECT count(*), membership FROM current_state_events
|
||||
WHERE type = 'm.room.member' AND room_id = ?
|
||||
AND membership IS NOT NULL
|
||||
GROUP BY membership
|
||||
"""
|
||||
|
||||
txn.execute(sql, (room_id,))
|
||||
return {membership: count for count, membership in txn}
|
||||
|
||||
@cached()
|
||||
async def get_number_joined_users_in_room(self, room_id: str) -> int:
|
||||
return await self.db_pool.simple_select_one_onecol(
|
||||
@@ -693,6 +711,27 @@ class RoomMemberWorkerStore(EventsWorkerStore, CacheInvalidationWorkerStore):
|
||||
|
||||
return {row[0] for row in txn}
|
||||
|
||||
async def get_rooms_user_currently_banned_from(
|
||||
self, user_id: str
|
||||
) -> FrozenSet[str]:
|
||||
"""Returns a set of room_ids the user is currently banned from.
|
||||
|
||||
If a remote user only returns rooms this server is currently
|
||||
participating in.
|
||||
"""
|
||||
room_ids = await self.db_pool.simple_select_onecol(
|
||||
table="current_state_events",
|
||||
keyvalues={
|
||||
"type": EventTypes.Member,
|
||||
"membership": Membership.BAN,
|
||||
"state_key": user_id,
|
||||
},
|
||||
retcol="room_id",
|
||||
desc="get_rooms_user_currently_banned_from",
|
||||
)
|
||||
|
||||
return frozenset(room_ids)
|
||||
|
||||
@cached(max_entries=500000, iterable=True)
|
||||
async def get_rooms_for_user(self, user_id: str) -> FrozenSet[str]:
|
||||
"""Returns a set of room_ids the user is currently joined to.
|
||||
@@ -1336,6 +1375,7 @@ class RoomMemberWorkerStore(EventsWorkerStore, CacheInvalidationWorkerStore):
|
||||
keyvalues={"user_id": user_id, "room_id": room_id},
|
||||
updatevalues={"forgotten": 1},
|
||||
)
|
||||
# Handle updating the `sliding_sync_membership_snapshots` table
|
||||
self.db_pool.simple_update_txn(
|
||||
txn,
|
||||
table="sliding_sync_membership_snapshots",
|
||||
@@ -1347,6 +1387,9 @@ class RoomMemberWorkerStore(EventsWorkerStore, CacheInvalidationWorkerStore):
|
||||
self._invalidate_cache_and_stream(
|
||||
txn, self.get_forgotten_rooms_for_user, (user_id,)
|
||||
)
|
||||
self._invalidate_cache_and_stream(
|
||||
txn, self.get_sliding_sync_rooms_for_user, (user_id,)
|
||||
)
|
||||
|
||||
await self.db_pool.runInteraction("forget_membership", f)
|
||||
|
||||
@@ -1383,7 +1426,7 @@ class RoomMemberWorkerStore(EventsWorkerStore, CacheInvalidationWorkerStore):
|
||||
) -> Mapping[str, RoomsForUserSlidingSync]:
|
||||
"""Get all the rooms for a user to handle a sliding sync request.
|
||||
|
||||
Ignores forgotten rooms and rooms that the user has been kicked from.
|
||||
Ignores forgotten rooms and rooms that the user has left themselves.
|
||||
|
||||
Returns:
|
||||
Map from room ID to membership info
|
||||
@@ -1392,10 +1435,15 @@ class RoomMemberWorkerStore(EventsWorkerStore, CacheInvalidationWorkerStore):
|
||||
def get_sliding_sync_rooms_for_user_txn(
|
||||
txn: LoggingTransaction,
|
||||
) -> Dict[str, RoomsForUserSlidingSync]:
|
||||
# XXX: If you use any new columns that can change (like from
|
||||
# `sliding_sync_joined_rooms` or `forgotten`), make sure to bust the
|
||||
# `get_sliding_sync_rooms_for_user` cache in the appropriate places (and add
|
||||
# tests).
|
||||
sql = """
|
||||
SELECT m.room_id, m.sender, m.membership, m.membership_event_id,
|
||||
r.room_version,
|
||||
m.event_instance_name, m.event_stream_ordering,
|
||||
m.has_known_state,
|
||||
COALESCE(j.room_type, m.room_type),
|
||||
COALESCE(j.is_encrypted, m.is_encrypted)
|
||||
FROM sliding_sync_membership_snapshots AS m
|
||||
@@ -1403,6 +1451,7 @@ class RoomMemberWorkerStore(EventsWorkerStore, CacheInvalidationWorkerStore):
|
||||
LEFT JOIN sliding_sync_joined_rooms AS j ON (j.room_id = m.room_id AND m.membership = 'join')
|
||||
WHERE user_id = ?
|
||||
AND m.forgotten = 0
|
||||
AND (m.membership != 'leave' OR m.user_id != m.sender)
|
||||
"""
|
||||
txn.execute(sql, (user_id,))
|
||||
return {
|
||||
@@ -1413,10 +1462,15 @@ class RoomMemberWorkerStore(EventsWorkerStore, CacheInvalidationWorkerStore):
|
||||
event_id=row[3],
|
||||
room_version_id=row[4],
|
||||
event_pos=PersistedEventPosition(row[5], row[6]),
|
||||
room_type=row[7],
|
||||
is_encrypted=row[8],
|
||||
has_known_state=bool(row[7]),
|
||||
room_type=row[8],
|
||||
is_encrypted=bool(row[9]),
|
||||
)
|
||||
for row in txn
|
||||
# We filter out unknown room versions proactively. They
|
||||
# shouldn't go down sync and their metadata may be in a broken
|
||||
# state (causing errors).
|
||||
if row[4] in KNOWN_ROOM_VERSIONS
|
||||
}
|
||||
|
||||
return await self.db_pool.runInteraction(
|
||||
@@ -1424,6 +1478,100 @@ class RoomMemberWorkerStore(EventsWorkerStore, CacheInvalidationWorkerStore):
|
||||
get_sliding_sync_rooms_for_user_txn,
|
||||
)
|
||||
|
||||
async def get_sliding_sync_room_for_user(
|
||||
self, user_id: str, room_id: str
|
||||
) -> Optional[RoomsForUserSlidingSync]:
|
||||
"""Get the sliding sync room entry for the given user and room."""
|
||||
|
||||
def get_sliding_sync_room_for_user_txn(
|
||||
txn: LoggingTransaction,
|
||||
) -> Optional[RoomsForUserSlidingSync]:
|
||||
sql = """
|
||||
SELECT m.room_id, m.sender, m.membership, m.membership_event_id,
|
||||
r.room_version,
|
||||
m.event_instance_name, m.event_stream_ordering,
|
||||
m.has_known_state,
|
||||
COALESCE(j.room_type, m.room_type),
|
||||
COALESCE(j.is_encrypted, m.is_encrypted)
|
||||
FROM sliding_sync_membership_snapshots AS m
|
||||
INNER JOIN rooms AS r USING (room_id)
|
||||
LEFT JOIN sliding_sync_joined_rooms AS j ON (j.room_id = m.room_id AND m.membership = 'join')
|
||||
WHERE user_id = ?
|
||||
AND m.forgotten = 0
|
||||
AND m.room_id = ?
|
||||
"""
|
||||
txn.execute(sql, (user_id, room_id))
|
||||
row = txn.fetchone()
|
||||
if not row:
|
||||
return None
|
||||
|
||||
return RoomsForUserSlidingSync(
|
||||
room_id=row[0],
|
||||
sender=row[1],
|
||||
membership=row[2],
|
||||
event_id=row[3],
|
||||
room_version_id=row[4],
|
||||
event_pos=PersistedEventPosition(row[5], row[6]),
|
||||
has_known_state=bool(row[7]),
|
||||
room_type=row[8],
|
||||
is_encrypted=row[9],
|
||||
)
|
||||
|
||||
return await self.db_pool.runInteraction(
|
||||
"get_sliding_sync_room_for_user", get_sliding_sync_room_for_user_txn
|
||||
)
|
||||
|
||||
async def get_sliding_sync_room_for_user_batch(
|
||||
self, user_id: str, room_ids: StrCollection
|
||||
) -> Dict[str, RoomsForUserSlidingSync]:
|
||||
"""Get the sliding sync room entry for the given user and rooms."""
|
||||
|
||||
if not room_ids:
|
||||
return {}
|
||||
|
||||
def get_sliding_sync_room_for_user_batch_txn(
|
||||
txn: LoggingTransaction,
|
||||
) -> Dict[str, RoomsForUserSlidingSync]:
|
||||
clause, args = make_in_list_sql_clause(
|
||||
self.database_engine, "m.room_id", room_ids
|
||||
)
|
||||
sql = f"""
|
||||
SELECT m.room_id, m.sender, m.membership, m.membership_event_id,
|
||||
r.room_version,
|
||||
m.event_instance_name, m.event_stream_ordering,
|
||||
m.has_known_state,
|
||||
COALESCE(j.room_type, m.room_type),
|
||||
COALESCE(j.is_encrypted, m.is_encrypted)
|
||||
FROM sliding_sync_membership_snapshots AS m
|
||||
INNER JOIN rooms AS r USING (room_id)
|
||||
LEFT JOIN sliding_sync_joined_rooms AS j ON (j.room_id = m.room_id AND m.membership = 'join')
|
||||
WHERE m.forgotten = 0
|
||||
AND {clause}
|
||||
AND user_id = ?
|
||||
"""
|
||||
args.append(user_id)
|
||||
txn.execute(sql, args)
|
||||
|
||||
return {
|
||||
row[0]: RoomsForUserSlidingSync(
|
||||
room_id=row[0],
|
||||
sender=row[1],
|
||||
membership=row[2],
|
||||
event_id=row[3],
|
||||
room_version_id=row[4],
|
||||
event_pos=PersistedEventPosition(row[5], row[6]),
|
||||
has_known_state=bool(row[7]),
|
||||
room_type=row[8],
|
||||
is_encrypted=row[9],
|
||||
)
|
||||
for row in txn
|
||||
}
|
||||
|
||||
return await self.db_pool.runInteraction(
|
||||
"get_sliding_sync_room_for_user_batch",
|
||||
get_sliding_sync_room_for_user_batch_txn,
|
||||
)
|
||||
|
||||
|
||||
class RoomMemberBackgroundUpdateStore(SQLBaseStore):
|
||||
def __init__(
|
||||
|
||||
@@ -267,6 +267,15 @@ class SlidingSyncStore(SQLBaseStore):
|
||||
(have_sent_room.status.value, have_sent_room.last_token)
|
||||
)
|
||||
|
||||
for (
|
||||
room_id,
|
||||
have_sent_room,
|
||||
) in per_connection_state.account_data._statuses.items():
|
||||
key_values.append((connection_position, "account_data", room_id))
|
||||
value_values.append(
|
||||
(have_sent_room.status.value, have_sent_room.last_token)
|
||||
)
|
||||
|
||||
self.db_pool.simple_upsert_many_txn(
|
||||
txn,
|
||||
table="sliding_sync_connection_streams",
|
||||
@@ -377,8 +386,8 @@ class SlidingSyncStore(SQLBaseStore):
|
||||
required_state_map: Dict[int, Dict[str, Set[str]]] = {}
|
||||
for row in rows:
|
||||
state = required_state_map[row[0]] = {}
|
||||
for event_type, state_keys in db_to_json(row[1]):
|
||||
state[event_type] = set(state_keys)
|
||||
for event_type, state_key in db_to_json(row[1]):
|
||||
state.setdefault(event_type, set()).add(state_key)
|
||||
|
||||
# Get all the room configs, looking up the required state from the map
|
||||
# above.
|
||||
@@ -407,6 +416,7 @@ class SlidingSyncStore(SQLBaseStore):
|
||||
# Now look up the per-room stream data.
|
||||
rooms: Dict[str, HaveSentRoom[str]] = {}
|
||||
receipts: Dict[str, HaveSentRoom[str]] = {}
|
||||
account_data: Dict[str, HaveSentRoom[str]] = {}
|
||||
|
||||
receipt_rows = self.db_pool.simple_select_list_txn(
|
||||
txn,
|
||||
@@ -427,6 +437,8 @@ class SlidingSyncStore(SQLBaseStore):
|
||||
rooms[room_id] = have_sent_room
|
||||
elif stream == "receipts":
|
||||
receipts[room_id] = have_sent_room
|
||||
elif stream == "account_data":
|
||||
account_data[room_id] = have_sent_room
|
||||
else:
|
||||
# For forwards compatibility we ignore unknown streams, as in
|
||||
# future we want to be able to easily add more stream types.
|
||||
@@ -435,6 +447,7 @@ class SlidingSyncStore(SQLBaseStore):
|
||||
return PerConnectionStateDB(
|
||||
rooms=RoomStatusMap(rooms),
|
||||
receipts=RoomStatusMap(receipts),
|
||||
account_data=RoomStatusMap(account_data),
|
||||
room_configs=room_configs,
|
||||
)
|
||||
|
||||
@@ -452,6 +465,7 @@ class PerConnectionStateDB:
|
||||
|
||||
rooms: "RoomStatusMap[str]"
|
||||
receipts: "RoomStatusMap[str]"
|
||||
account_data: "RoomStatusMap[str]"
|
||||
|
||||
room_configs: Mapping[str, "RoomSyncConfig"]
|
||||
|
||||
@@ -484,10 +498,21 @@ class PerConnectionStateDB:
|
||||
for room_id, status in per_connection_state.receipts.get_updates().items()
|
||||
}
|
||||
|
||||
account_data = {
|
||||
room_id: HaveSentRoom(
|
||||
status=status.status,
|
||||
last_token=(
|
||||
str(status.last_token) if status.last_token is not None else None
|
||||
),
|
||||
)
|
||||
for room_id, status in per_connection_state.account_data.get_updates().items()
|
||||
}
|
||||
|
||||
log_kv(
|
||||
{
|
||||
"rooms": rooms,
|
||||
"receipts": receipts,
|
||||
"account_data": account_data,
|
||||
"room_configs": per_connection_state.room_configs.maps[0],
|
||||
}
|
||||
)
|
||||
@@ -495,6 +520,7 @@ class PerConnectionStateDB:
|
||||
return PerConnectionStateDB(
|
||||
rooms=RoomStatusMap(rooms),
|
||||
receipts=RoomStatusMap(receipts),
|
||||
account_data=RoomStatusMap(account_data),
|
||||
room_configs=per_connection_state.room_configs.maps[0],
|
||||
)
|
||||
|
||||
@@ -524,8 +550,19 @@ class PerConnectionStateDB:
|
||||
for room_id, status in self.receipts._statuses.items()
|
||||
}
|
||||
|
||||
account_data = {
|
||||
room_id: HaveSentRoom(
|
||||
status=status.status,
|
||||
last_token=(
|
||||
int(status.last_token) if status.last_token is not None else None
|
||||
),
|
||||
)
|
||||
for room_id, status in self.account_data._statuses.items()
|
||||
}
|
||||
|
||||
return PerConnectionState(
|
||||
rooms=RoomStatusMap(rooms),
|
||||
receipts=RoomStatusMap(receipts),
|
||||
account_data=RoomStatusMap(account_data),
|
||||
room_configs=self.room_configs,
|
||||
)
|
||||
|
||||
@@ -308,8 +308,24 @@ class StateGroupWorkerStore(EventsWorkerStore, SQLBaseStore):
|
||||
return create_event
|
||||
|
||||
@cached(max_entries=10000)
|
||||
async def get_room_type(self, room_id: str) -> Optional[str]:
|
||||
raise NotImplementedError()
|
||||
async def get_room_type(self, room_id: str) -> Union[Optional[str], Sentinel]:
|
||||
"""Fetch room type for given room.
|
||||
|
||||
Since this function is cached, any missing values would be cached as
|
||||
`None`. In order to distinguish between an unencrypted room that has
|
||||
`None` encryption and a room that is unknown to the server where we
|
||||
might want to omit the value (which would make it cached as `None`),
|
||||
instead we use the sentinel value `ROOM_UNKNOWN_SENTINEL`.
|
||||
"""
|
||||
|
||||
try:
|
||||
create_event = await self.get_create_event_for_room(room_id)
|
||||
return create_event.content.get(EventContentFields.ROOM_TYPE)
|
||||
except NotFoundError:
|
||||
# We use the sentinel value to distinguish between `None` which is a
|
||||
# valid room type and a room that is unknown to the server so the value
|
||||
# is just unset.
|
||||
return ROOM_UNKNOWN_SENTINEL
|
||||
|
||||
@cachedList(cached_method_name="get_room_type", list_name="room_ids")
|
||||
async def bulk_get_room_type(
|
||||
@@ -736,6 +752,7 @@ class MainStateBackgroundUpdateStore(RoomMemberWorkerStore):
|
||||
CURRENT_STATE_INDEX_UPDATE_NAME = "current_state_members_idx"
|
||||
EVENT_STATE_GROUP_INDEX_UPDATE_NAME = "event_to_state_groups_sg_index"
|
||||
DELETE_CURRENT_STATE_UPDATE_NAME = "delete_old_current_state_events"
|
||||
MEMBERS_CURRENT_STATE_UPDATE_NAME = "current_state_events_members_room_index"
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
@@ -764,6 +781,13 @@ class MainStateBackgroundUpdateStore(RoomMemberWorkerStore):
|
||||
self.DELETE_CURRENT_STATE_UPDATE_NAME,
|
||||
self._background_remove_left_rooms,
|
||||
)
|
||||
self.db_pool.updates.register_background_index_update(
|
||||
self.MEMBERS_CURRENT_STATE_UPDATE_NAME,
|
||||
index_name="current_state_events_members_room_index",
|
||||
table="current_state_events",
|
||||
columns=["room_id", "membership"],
|
||||
where_clause="type='m.room.member'",
|
||||
)
|
||||
|
||||
async def _background_remove_left_rooms(
|
||||
self, progress: JsonDict, batch_size: int
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user