Compare commits
50 Commits
v1.59.1
...
dmr/reject
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0953cad3e4 | ||
|
|
ad6a6675bf | ||
|
|
21d1347f2c | ||
|
|
a9fe3350f8 | ||
|
|
a1adede444 | ||
|
|
79f1cef5e4 | ||
|
|
8c977edec8 | ||
|
|
1402159bb8 | ||
|
|
32ef24fbd7 | ||
|
|
fcf951d5dc | ||
|
|
1fe202a1a3 | ||
|
|
6d8d1218dd | ||
|
|
3eafee629d | ||
|
|
e24c11afd6 | ||
|
|
83be72d76c | ||
|
|
3ce15cc7be | ||
|
|
b4eb163434 | ||
|
|
8060034612 | ||
|
|
a5c26750b5 | ||
|
|
86a515ccbf | ||
|
|
cde8af9a49 | ||
|
|
e8ae472d3b | ||
|
|
9013104429 | ||
|
|
aec69d2481 | ||
|
|
39bed28b28 | ||
|
|
c9fc2c0d22 | ||
|
|
57f6c496d0 | ||
|
|
17e1eb7749 | ||
|
|
de1e599b9d | ||
|
|
409573f6d0 | ||
|
|
bf7ce92bf7 | ||
|
|
db10f2c037 | ||
|
|
6ee61b9052 | ||
|
|
d38d242411 | ||
|
|
a559c8b0d9 | ||
|
|
9d8e380d2e | ||
|
|
dffecade7d | ||
|
|
a4c75918b3 | ||
|
|
84facf769e | ||
|
|
c72d26c1e1 | ||
|
|
c997bfb926 | ||
|
|
29f06704b8 | ||
|
|
989fa33096 | ||
|
|
147f098fb4 | ||
|
|
dbb12a0b54 | ||
|
|
5cfb004595 | ||
|
|
5c00151c28 | ||
|
|
2aad0ae57f | ||
|
|
b44fbdffa4 | ||
|
|
02cdace707 |
1
changelog.d/10533.misc
Normal file
1
changelog.d/10533.misc
Normal file
@@ -0,0 +1 @@
|
||||
Improve event caching mechanism to avoid having multiple copies of an event in memory at a time.
|
||||
1
changelog.d/12477.misc
Normal file
1
changelog.d/12477.misc
Normal file
@@ -0,0 +1 @@
|
||||
Add some type hints to datastore.
|
||||
1
changelog.d/12513.feature
Normal file
1
changelog.d/12513.feature
Normal file
@@ -0,0 +1 @@
|
||||
Measure the time taken in spam-checking callbacks and expose those measurements as metrics.
|
||||
1
changelog.d/12567.misc
Normal file
1
changelog.d/12567.misc
Normal file
@@ -0,0 +1 @@
|
||||
Replace string literal instances of stream key types with typed constants.
|
||||
1
changelog.d/12586.misc
Normal file
1
changelog.d/12586.misc
Normal file
@@ -0,0 +1 @@
|
||||
Add `@cancellable` decorator, for use on endpoint methods that can be cancelled when clients disconnect.
|
||||
1
changelog.d/12588.misc
Normal file
1
changelog.d/12588.misc
Normal file
@@ -0,0 +1 @@
|
||||
Add ability to cancel disconnected requests to `SynapseRequest`.
|
||||
1
changelog.d/12618.feature
Normal file
1
changelog.d/12618.feature
Normal file
@@ -0,0 +1 @@
|
||||
Add a `default_power_level_content_override` config option to set default room power levels per room preset.
|
||||
1
changelog.d/12630.misc
Normal file
1
changelog.d/12630.misc
Normal file
@@ -0,0 +1 @@
|
||||
Add a helper class for testing request cancellation.
|
||||
1
changelog.d/12673.feature
Normal file
1
changelog.d/12673.feature
Normal file
@@ -0,0 +1 @@
|
||||
Synapse will now reload [cache config](https://matrix-org.github.io/synapse/latest/usage/configuration/config_documentation.html#caching) when it receives a [SIGHUP](https://en.wikipedia.org/wiki/SIGHUP) signal.
|
||||
1
changelog.d/12676.misc
Normal file
1
changelog.d/12676.misc
Normal file
@@ -0,0 +1 @@
|
||||
Improve documentation of the `synapse.push` module.
|
||||
1
changelog.d/12677.misc
Normal file
1
changelog.d/12677.misc
Normal file
@@ -0,0 +1 @@
|
||||
Refactor functions to on `PushRuleEvaluatorForEvent`.
|
||||
1
changelog.d/12679.misc
Normal file
1
changelog.d/12679.misc
Normal file
@@ -0,0 +1 @@
|
||||
Preparation for database schema simplifications: stop writing to `event_reference_hashes`.
|
||||
1
changelog.d/12683.bugfix
Normal file
1
changelog.d/12683.bugfix
Normal file
@@ -0,0 +1 @@
|
||||
Fix a bug introduced in Synapse 1.57.0 where `/messages` would throw a 500 error when querying for a non-existent room.
|
||||
1
changelog.d/12689.misc
Normal file
1
changelog.d/12689.misc
Normal file
@@ -0,0 +1 @@
|
||||
Refactor `EventContext` class.
|
||||
1
changelog.d/12691.misc
Normal file
1
changelog.d/12691.misc
Normal file
@@ -0,0 +1 @@
|
||||
Remove an unneeded class in the push code.
|
||||
1
changelog.d/12693.misc
Normal file
1
changelog.d/12693.misc
Normal file
@@ -0,0 +1 @@
|
||||
Consolidate parsing of relation information from events.
|
||||
1
changelog.d/12694.misc
Normal file
1
changelog.d/12694.misc
Normal file
@@ -0,0 +1 @@
|
||||
Capture the `Deferred` for request cancellation in `_AsyncResource`.
|
||||
1
changelog.d/12695.misc
Normal file
1
changelog.d/12695.misc
Normal file
@@ -0,0 +1 @@
|
||||
Fixes an incorrect type hint for `Filter._check_event_relations`.
|
||||
1
changelog.d/12696.bugfix
Normal file
1
changelog.d/12696.bugfix
Normal file
@@ -0,0 +1 @@
|
||||
Fix a long-standing bug where an empty room would be created when a user with an insufficient power level tried to upgrade a room.
|
||||
1
changelog.d/12698.misc
Normal file
1
changelog.d/12698.misc
Normal file
@@ -0,0 +1 @@
|
||||
Respect the `@cancellable` flag for `DirectServe{Html,Json}Resource`s.
|
||||
1
changelog.d/12699.misc
Normal file
1
changelog.d/12699.misc
Normal file
@@ -0,0 +1 @@
|
||||
Respect the `@cancellable` flag for `RestServlet`s and `BaseFederationServlet`s.
|
||||
1
changelog.d/12700.misc
Normal file
1
changelog.d/12700.misc
Normal file
@@ -0,0 +1 @@
|
||||
Respect the `@cancellable` flag for `ReplicationEndpoint`s.
|
||||
1
changelog.d/12701.feature
Normal file
1
changelog.d/12701.feature
Normal file
@@ -0,0 +1 @@
|
||||
Add a config options to allow for auto-tuning of caches.
|
||||
1
changelog.d/12705.misc
Normal file
1
changelog.d/12705.misc
Normal file
@@ -0,0 +1 @@
|
||||
Complain if a federation endpoint has the `@cancellable` flag, since some of the wrapper code may not handle cancellation correctly yet.
|
||||
1
changelog.d/12708.misc
Normal file
1
changelog.d/12708.misc
Normal file
@@ -0,0 +1 @@
|
||||
Enable cancellation of `GET /rooms/$room_id/members`, `GET /rooms/$room_id/state` and `GET /rooms/$room_id/state/$event_type/*` requests.
|
||||
1
changelog.d/12709.removal
Normal file
1
changelog.d/12709.removal
Normal file
@@ -0,0 +1 @@
|
||||
Require a body in POST requests to `/rooms/{roomId}/receipt/{receiptType}/{eventId}`, as required by the [Matrix specification](https://spec.matrix.org/v1.2/client-server-api/#post_matrixclientv3roomsroomidreceiptreceipttypeeventid). This breaks compatibility with Element Android 1.2.0 and earlier: users of those clients will be unable to send read receipts.
|
||||
1
changelog.d/12711.misc
Normal file
1
changelog.d/12711.misc
Normal file
@@ -0,0 +1 @@
|
||||
Optimize private read receipt filtering.
|
||||
1
changelog.d/12713.bugfix
Normal file
1
changelog.d/12713.bugfix
Normal file
@@ -0,0 +1 @@
|
||||
Fix a bug introduced in Synapse 1.30.0 where empty rooms could be automatically created if a monthly active users limit is set.
|
||||
1
changelog.d/12715.doc
Normal file
1
changelog.d/12715.doc
Normal file
@@ -0,0 +1 @@
|
||||
Fix a typo in the Media Admin API documentation.
|
||||
1
changelog.d/12716.misc
Normal file
1
changelog.d/12716.misc
Normal file
@@ -0,0 +1 @@
|
||||
Add type annotations to increase the number of modules passing `disallow-untyped-defs`.
|
||||
1
changelog.d/12720.misc
Normal file
1
changelog.d/12720.misc
Normal file
@@ -0,0 +1 @@
|
||||
Drop the logging level of status messages for the URL preview cache expiry job from INFO to DEBUG.
|
||||
1
changelog.d/12726.misc
Normal file
1
changelog.d/12726.misc
Normal file
@@ -0,0 +1 @@
|
||||
Add type annotations to increase the number of modules passing `disallow-untyped-defs`.
|
||||
1
changelog.d/12727.doc
Normal file
1
changelog.d/12727.doc
Normal file
@@ -0,0 +1 @@
|
||||
Update the OpenID Connect example for Keycloak to be compatible with newer versions of Keycloak. Contributed by @nhh.
|
||||
1
changelog.d/12731.misc
Normal file
1
changelog.d/12731.misc
Normal file
@@ -0,0 +1 @@
|
||||
Update configs used by Complement to allow more invites/3PID validations during tests.
|
||||
1
changelog.d/12734.misc
Normal file
1
changelog.d/12734.misc
Normal file
@@ -0,0 +1 @@
|
||||
Tidy up and type-hint the database engine modules.
|
||||
1
changelog.d/12742.doc
Normal file
1
changelog.d/12742.doc
Normal file
@@ -0,0 +1 @@
|
||||
Fix typo in server listener documentation.
|
||||
1
changelog.d/12747.bugfix
Normal file
1
changelog.d/12747.bugfix
Normal file
@@ -0,0 +1 @@
|
||||
Fix poor database performance when reading the cache invalidation stream for large servers with lots of workers.
|
||||
1
changelog.d/12749.doc
Normal file
1
changelog.d/12749.doc
Normal file
@@ -0,0 +1 @@
|
||||
Fix typo in 'run_background_tasks_on' option name in configuration manual documentation.
|
||||
1
changelog.d/12762.misc
Normal file
1
changelog.d/12762.misc
Normal file
@@ -0,0 +1 @@
|
||||
Fix a long-standing bug where the user directory background process would fail to make forward progress if a user included a null codepoint in their display name or avatar.
|
||||
@@ -53,6 +53,18 @@ rc_joins:
|
||||
per_second: 9999
|
||||
burst_count: 9999
|
||||
|
||||
rc_3pid_validation:
|
||||
per_second: 1000
|
||||
burst_count: 1000
|
||||
|
||||
rc_invites:
|
||||
per_room:
|
||||
per_second: 1000
|
||||
burst_count: 1000
|
||||
per_user:
|
||||
per_second: 1000
|
||||
burst_count: 1000
|
||||
|
||||
federation_rr_transactions_per_room_per_second: 9999
|
||||
|
||||
## Experimental Features ##
|
||||
|
||||
@@ -87,6 +87,18 @@ rc_joins:
|
||||
per_second: 9999
|
||||
burst_count: 9999
|
||||
|
||||
rc_3pid_validation:
|
||||
per_second: 1000
|
||||
burst_count: 1000
|
||||
|
||||
rc_invites:
|
||||
per_room:
|
||||
per_second: 1000
|
||||
burst_count: 1000
|
||||
per_user:
|
||||
per_second: 1000
|
||||
burst_count: 1000
|
||||
|
||||
federation_rr_transactions_per_room_per_second: 9999
|
||||
|
||||
## API Configuration ##
|
||||
|
||||
@@ -289,7 +289,7 @@ POST /_synapse/admin/v1/purge_media_cache?before_ts=<unix_timestamp_in_ms>
|
||||
|
||||
URL Parameters
|
||||
|
||||
* `unix_timestamp_in_ms`: string representing a positive integer - Unix timestamp in milliseconds.
|
||||
* `before_ts`: string representing a positive integer - Unix timestamp in milliseconds.
|
||||
All cached media that was last accessed before this timestamp will be removed.
|
||||
|
||||
Response:
|
||||
|
||||
@@ -159,7 +159,7 @@ Follow the [Getting Started Guide](https://www.keycloak.org/getting-started) to
|
||||
oidc_providers:
|
||||
- idp_id: keycloak
|
||||
idp_name: "My KeyCloak server"
|
||||
issuer: "https://127.0.0.1:8443/auth/realms/{realm_name}"
|
||||
issuer: "https://127.0.0.1:8443/realms/{realm_name}"
|
||||
client_id: "synapse"
|
||||
client_secret: "copy secret generated from above"
|
||||
scopes: ["openid", "profile"]
|
||||
|
||||
@@ -289,7 +289,7 @@ presence:
|
||||
# federation: the server-server API (/_matrix/federation). Also implies
|
||||
# 'media', 'keys', 'openid'
|
||||
#
|
||||
# keys: the key discovery API (/_matrix/keys).
|
||||
# keys: the key discovery API (/_matrix/key).
|
||||
#
|
||||
# media: the media API (/_matrix/media).
|
||||
#
|
||||
@@ -730,6 +730,12 @@ retention:
|
||||
# A cache 'factor' is a multiplier that can be applied to each of
|
||||
# Synapse's caches in order to increase or decrease the maximum
|
||||
# number of entries that can be stored.
|
||||
#
|
||||
# The configuration for cache factors (caches.global_factor and
|
||||
# caches.per_cache_factors) can be reloaded while the application is running,
|
||||
# by sending a SIGHUP signal to the Synapse process. Changes to other parts of
|
||||
# the caching config will NOT be applied after a SIGHUP is received; a restart
|
||||
# is necessary.
|
||||
|
||||
# The number of events to cache in memory. Not affected by
|
||||
# caches.global_factor.
|
||||
@@ -778,6 +784,24 @@ caches:
|
||||
#
|
||||
#cache_entry_ttl: 30m
|
||||
|
||||
# This flag enables cache autotuning, and is further specified by the sub-options `max_cache_memory_usage`,
|
||||
# `target_cache_memory_usage`, `min_cache_ttl`. These flags work in conjunction with each other to maintain
|
||||
# a balance between cache memory usage and cache entry availability. You must be using jemalloc to utilize
|
||||
# this option, and all three of the options must be specified for this feature to work.
|
||||
#cache_autotuning:
|
||||
# This flag sets a ceiling on 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
|
||||
# the flag below, or until the `min_cache_ttl` is hit.
|
||||
#max_cache_memory_usage: 1024M
|
||||
|
||||
# This flag sets a rough target for the desired memory usage of the caches.
|
||||
#target_cache_memory_usage: 758M
|
||||
|
||||
# 'min_cache_ttl` sets a limit under which newer cache entries are not evicted and is only applied when
|
||||
# caches are actively being evicted/`max_cache_memory_usage` has been exceeded. This is to protect hot caches
|
||||
# from being emptied while Synapse is evicting due to memory.
|
||||
#min_cache_ttl: 5m
|
||||
|
||||
# Controls how long the results of a /sync request are cached for after
|
||||
# a successful response is returned. A higher duration can help clients with
|
||||
# intermittent connections, at the cost of higher memory usage.
|
||||
@@ -2462,6 +2486,40 @@ push:
|
||||
#
|
||||
#encryption_enabled_by_default_for_room_type: invite
|
||||
|
||||
# Override the default power levels for rooms created on this server, per
|
||||
# room creation preset.
|
||||
#
|
||||
# The appropriate dictionary for the room preset will be applied on top
|
||||
# of the existing power levels content.
|
||||
#
|
||||
# Useful if you know that your users need special permissions in rooms
|
||||
# that they create (e.g. to send particular types of state events without
|
||||
# needing an elevated power level). This takes the same shape as the
|
||||
# `power_level_content_override` parameter in the /createRoom API, but
|
||||
# is applied before that parameter.
|
||||
#
|
||||
# Valid keys are some or all of `private_chat`, `trusted_private_chat`
|
||||
# and `public_chat`. Inside each of those should be any of the
|
||||
# properties allowed in `power_level_content_override` in the
|
||||
# /createRoom API. If any property is missing, its default value will
|
||||
# continue to be used. If any property is present, it will overwrite
|
||||
# the existing default completely (so if the `events` property exists,
|
||||
# the default event power levels will be ignored).
|
||||
#
|
||||
#default_power_level_content_override:
|
||||
# private_chat:
|
||||
# "events":
|
||||
# "com.example.myeventtype" : 0
|
||||
# "m.room.avatar": 50
|
||||
# "m.room.canonical_alias": 50
|
||||
# "m.room.encryption": 100
|
||||
# "m.room.history_visibility": 100
|
||||
# "m.room.name": 50
|
||||
# "m.room.power_levels": 100
|
||||
# "m.room.server_acl": 100
|
||||
# "m.room.tombstone": 100
|
||||
# "events_default": 1
|
||||
|
||||
|
||||
# Uncomment to allow non-server-admin users to create groups on this server
|
||||
#
|
||||
|
||||
@@ -467,13 +467,13 @@ Sub-options for each listener include:
|
||||
|
||||
Valid resource names are:
|
||||
|
||||
* `client`: the client-server API (/_matrix/client), and the synapse admin API (/_synapse/admin). Also implies 'media' and 'static'.
|
||||
* `client`: the client-server API (/_matrix/client), and the synapse admin API (/_synapse/admin). Also implies `media` and `static`.
|
||||
|
||||
* `consent`: user consent forms (/_matrix/consent). See [here](../../consent_tracking.md) for more.
|
||||
|
||||
* `federation`: the server-server API (/_matrix/federation). Also implies `media`, `keys`, `openid`
|
||||
|
||||
* `keys`: the key discovery API (/_matrix/keys).
|
||||
* `keys`: the key discovery API (/_matrix/key).
|
||||
|
||||
* `media`: the media API (/_matrix/media).
|
||||
|
||||
@@ -1119,7 +1119,17 @@ Caching can be configured through the following sub-options:
|
||||
with intermittent connections, at the cost of higher memory usage.
|
||||
By default, this is zero, which means that sync responses are not cached
|
||||
at all.
|
||||
|
||||
* `cache_autotuning` and its sub-options `max_cache_memory_usage`, `target_cache_memory_usage`, and
|
||||
`min_cache_ttl` work in conjunction with each other to maintain a balance between cache memory
|
||||
usage and cache entry availability. You must be using [jemalloc](https://github.com/matrix-org/synapse#help-synapse-is-slow-and-eats-all-my-ramcpu)
|
||||
to utilize this option, and all three of the options must be specified for this feature to work.
|
||||
* `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
|
||||
the flag below, or until the `min_cache_ttl` is hit.
|
||||
* `target_memory_usage` sets a rough target for the desired memory usage of the caches.
|
||||
* `min_cache_ttl` sets a limit under which newer cache entries are not evicted and is only applied when
|
||||
caches are actively being evicted/`max_cache_memory_usage` has been exceeded. This is to protect hot caches
|
||||
from being emptied while Synapse is evicting due to memory.
|
||||
|
||||
Example configuration:
|
||||
```yaml
|
||||
@@ -1127,9 +1137,29 @@ caches:
|
||||
global_factor: 1.0
|
||||
per_cache_factors:
|
||||
get_users_who_share_room_with_user: 2.0
|
||||
expire_caches: false
|
||||
sync_response_cache_duration: 2m
|
||||
cache_autotuning:
|
||||
max_cache_memory_usage: 1024M
|
||||
target_cache_memory_usage: 758M
|
||||
min_cache_ttl: 5m
|
||||
```
|
||||
|
||||
### Reloading cache factors
|
||||
|
||||
The cache factors (i.e. `caches.global_factor` and `caches.per_cache_factors`) may be reloaded at any time by sending a
|
||||
[`SIGHUP`](https://en.wikipedia.org/wiki/SIGHUP) signal to Synapse using e.g.
|
||||
|
||||
```commandline
|
||||
kill -HUP [PID_OF_SYNAPSE_PROCESS]
|
||||
```
|
||||
|
||||
If you are running multiple workers, you must individually update the worker
|
||||
config file and send this signal to each worker process.
|
||||
|
||||
If you're using the [example systemd service](https://github.com/matrix-org/synapse/blob/develop/contrib/systemd/matrix-synapse.service)
|
||||
file in Synapse's `contrib` directory, you can send a `SIGHUP` signal by using
|
||||
`systemctl reload matrix-synapse`.
|
||||
|
||||
---
|
||||
## Database ##
|
||||
Config options related to database settings.
|
||||
@@ -3298,6 +3328,32 @@ room_list_publication_rules:
|
||||
room_id: "*"
|
||||
action: allow
|
||||
```
|
||||
|
||||
---
|
||||
Config option: `default_power_level_content_override`
|
||||
|
||||
The `default_power_level_content_override` option controls the default power
|
||||
levels for rooms.
|
||||
|
||||
Useful if you know that your users need special permissions in rooms
|
||||
that they create (e.g. to send particular types of state events without
|
||||
needing an elevated power level). This takes the same shape as the
|
||||
`power_level_content_override` parameter in the /createRoom API, but
|
||||
is applied before that parameter.
|
||||
|
||||
Note that each key provided inside a preset (for example `events` in the example
|
||||
below) will overwrite all existing defaults inside that key. So in the example
|
||||
below, newly-created private_chat rooms will have no rules for any event types
|
||||
except `com.example.foo`.
|
||||
|
||||
Example configuration:
|
||||
```yaml
|
||||
default_power_level_content_override:
|
||||
private_chat: { "events": { "com.example.foo" : 0 } }
|
||||
trusted_private_chat: null
|
||||
public_chat: null
|
||||
```
|
||||
|
||||
---
|
||||
## Opentracing ##
|
||||
Configuration options related to Opentracing support.
|
||||
@@ -3398,7 +3454,7 @@ stream_writers:
|
||||
typing: worker1
|
||||
```
|
||||
---
|
||||
Config option: `run_background_task_on`
|
||||
Config option: `run_background_tasks_on`
|
||||
|
||||
The worker that is used to run background tasks (e.g. cleaning up expired
|
||||
data). If not provided this defaults to the main process.
|
||||
|
||||
48
mypy.ini
48
mypy.ini
@@ -119,15 +119,39 @@ disallow_untyped_defs = True
|
||||
[mypy-synapse.federation.transport.client]
|
||||
disallow_untyped_defs = False
|
||||
|
||||
[mypy-synapse.groups.*]
|
||||
disallow_untyped_defs = True
|
||||
|
||||
[mypy-synapse.handlers.*]
|
||||
disallow_untyped_defs = True
|
||||
|
||||
[mypy-synapse.http.federation.*]
|
||||
disallow_untyped_defs = True
|
||||
|
||||
[mypy-synapse.http.connectproxyclient]
|
||||
disallow_untyped_defs = True
|
||||
|
||||
[mypy-synapse.http.proxyagent]
|
||||
disallow_untyped_defs = True
|
||||
|
||||
[mypy-synapse.http.request_metrics]
|
||||
disallow_untyped_defs = True
|
||||
|
||||
[mypy-synapse.http.server]
|
||||
disallow_untyped_defs = True
|
||||
|
||||
[mypy-synapse.logging._remote]
|
||||
disallow_untyped_defs = True
|
||||
|
||||
[mypy-synapse.logging.context]
|
||||
disallow_untyped_defs = True
|
||||
|
||||
[mypy-synapse.logging.formatter]
|
||||
disallow_untyped_defs = True
|
||||
|
||||
[mypy-synapse.logging.handlers]
|
||||
disallow_untyped_defs = True
|
||||
|
||||
[mypy-synapse.metrics.*]
|
||||
disallow_untyped_defs = True
|
||||
|
||||
@@ -157,6 +181,9 @@ disallow_untyped_defs = True
|
||||
[mypy-synapse.state.*]
|
||||
disallow_untyped_defs = True
|
||||
|
||||
[mypy-synapse.storage.databases.background_updates]
|
||||
disallow_untyped_defs = True
|
||||
|
||||
[mypy-synapse.storage.databases.main.account_data]
|
||||
disallow_untyped_defs = True
|
||||
|
||||
@@ -196,18 +223,39 @@ disallow_untyped_defs = True
|
||||
[mypy-synapse.storage.databases.main.state_deltas]
|
||||
disallow_untyped_defs = True
|
||||
|
||||
[mypy-synapse.storage.databases.main.stream]
|
||||
disallow_untyped_defs = True
|
||||
|
||||
[mypy-synapse.storage.databases.main.transactions]
|
||||
disallow_untyped_defs = True
|
||||
|
||||
[mypy-synapse.storage.databases.main.user_erasure_store]
|
||||
disallow_untyped_defs = True
|
||||
|
||||
[mypy-synapse.storage.engines.*]
|
||||
disallow_untyped_defs = True
|
||||
|
||||
[mypy-synapse.storage.prepare_database]
|
||||
disallow_untyped_defs = True
|
||||
|
||||
[mypy-synapse.storage.persist_events]
|
||||
disallow_untyped_defs = True
|
||||
|
||||
[mypy-synapse.storage.state]
|
||||
disallow_untyped_defs = True
|
||||
|
||||
[mypy-synapse.storage.types]
|
||||
disallow_untyped_defs = True
|
||||
|
||||
[mypy-synapse.storage.util.*]
|
||||
disallow_untyped_defs = True
|
||||
|
||||
[mypy-synapse.streams.*]
|
||||
disallow_untyped_defs = True
|
||||
|
||||
[mypy-synapse.types]
|
||||
disallow_untyped_defs = True
|
||||
|
||||
[mypy-synapse.util.*]
|
||||
disallow_untyped_defs = True
|
||||
|
||||
|
||||
@@ -19,6 +19,7 @@ from typing import (
|
||||
TYPE_CHECKING,
|
||||
Awaitable,
|
||||
Callable,
|
||||
Collection,
|
||||
Dict,
|
||||
Iterable,
|
||||
List,
|
||||
@@ -444,9 +445,9 @@ class Filter:
|
||||
return room_ids
|
||||
|
||||
async def _check_event_relations(
|
||||
self, events: Iterable[FilterEvent]
|
||||
self, events: Collection[FilterEvent]
|
||||
) -> List[FilterEvent]:
|
||||
# The event IDs to check, mypy doesn't understand the ifinstance check.
|
||||
# The event IDs to check, mypy doesn't understand the isinstance check.
|
||||
event_ids = [event.event_id for event in events if isinstance(event, EventBase)] # type: ignore[attr-defined]
|
||||
event_ids_to_keep = set(
|
||||
await self._store.events_have_relations(
|
||||
|
||||
@@ -49,9 +49,12 @@ from twisted.logger import LoggingFile, LogLevel
|
||||
from twisted.protocols.tls import TLSMemoryBIOFactory
|
||||
from twisted.python.threadpool import ThreadPool
|
||||
|
||||
import synapse.util.caches
|
||||
from synapse.api.constants import MAX_PDU_SIZE
|
||||
from synapse.app import check_bind_error
|
||||
from synapse.app.phone_stats_home import start_phone_stats_home
|
||||
from synapse.config import ConfigError
|
||||
from synapse.config._base import format_config_error
|
||||
from synapse.config.homeserver import HomeServerConfig
|
||||
from synapse.config.server import ManholeConfig
|
||||
from synapse.crypto import context_factory
|
||||
@@ -432,6 +435,10 @@ async def start(hs: "HomeServer") -> None:
|
||||
signal.signal(signal.SIGHUP, run_sighup)
|
||||
|
||||
register_sighup(refresh_certificate, hs)
|
||||
register_sighup(reload_cache_config, hs.config)
|
||||
|
||||
# Apply the cache config.
|
||||
hs.config.caches.resize_all_caches()
|
||||
|
||||
# Load the certificate from disk.
|
||||
refresh_certificate(hs)
|
||||
@@ -486,6 +493,43 @@ async def start(hs: "HomeServer") -> None:
|
||||
atexit.register(gc.freeze)
|
||||
|
||||
|
||||
def reload_cache_config(config: HomeServerConfig) -> None:
|
||||
"""Reload cache config from disk and immediately apply it.resize caches accordingly.
|
||||
|
||||
If the config is invalid, a `ConfigError` is logged and no changes are made.
|
||||
|
||||
Otherwise, this:
|
||||
- replaces the `caches` section on the given `config` object,
|
||||
- resizes all caches according to the new cache factors, and
|
||||
|
||||
Note that the following cache config keys are read, but not applied:
|
||||
- event_cache_size: used to set a max_size and _original_max_size on
|
||||
EventsWorkerStore._get_event_cache when it is created. We'd have to update
|
||||
the _original_max_size (and maybe
|
||||
- sync_response_cache_duration: would have to update the timeout_sec attribute on
|
||||
HomeServer -> SyncHandler -> ResponseCache.
|
||||
- track_memory_usage. This affects synapse.util.caches.TRACK_MEMORY_USAGE which
|
||||
influences Synapse's self-reported metrics.
|
||||
|
||||
Also, the HTTPConnectionPool in SimpleHTTPClient sets its maxPersistentPerHost
|
||||
parameter based on the global_factor. This won't be applied on a config reload.
|
||||
"""
|
||||
try:
|
||||
previous_cache_config = config.reload_config_section("caches")
|
||||
except ConfigError as e:
|
||||
logger.warning("Failed to reload cache config")
|
||||
for f in format_config_error(e):
|
||||
logger.warning(f)
|
||||
else:
|
||||
logger.debug(
|
||||
"New cache config. Was:\n %s\nNow:\n",
|
||||
previous_cache_config.__dict__,
|
||||
config.caches.__dict__,
|
||||
)
|
||||
synapse.util.caches.TRACK_MEMORY_USAGE = config.caches.track_memory_usage
|
||||
config.caches.resize_all_caches()
|
||||
|
||||
|
||||
def setup_sentry(hs: "HomeServer") -> None:
|
||||
"""Enable sentry integration, if enabled in configuration"""
|
||||
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
import logging
|
||||
import os
|
||||
import sys
|
||||
from typing import Dict, Iterable, Iterator, List
|
||||
from typing import Dict, Iterable, List
|
||||
|
||||
from matrix_common.versionstring import get_distribution_version_string
|
||||
|
||||
@@ -45,7 +45,7 @@ from synapse.app._base import (
|
||||
redirect_stdio_to_logs,
|
||||
register_start,
|
||||
)
|
||||
from synapse.config._base import ConfigError
|
||||
from synapse.config._base import ConfigError, format_config_error
|
||||
from synapse.config.emailconfig import ThreepidBehaviour
|
||||
from synapse.config.homeserver import HomeServerConfig
|
||||
from synapse.config.server import ListenerConfig
|
||||
@@ -399,38 +399,6 @@ def setup(config_options: List[str]) -> SynapseHomeServer:
|
||||
return hs
|
||||
|
||||
|
||||
def format_config_error(e: ConfigError) -> Iterator[str]:
|
||||
"""
|
||||
Formats a config error neatly
|
||||
|
||||
The idea is to format the immediate error, plus the "causes" of those errors,
|
||||
hopefully in a way that makes sense to the user. For example:
|
||||
|
||||
Error in configuration at 'oidc_config.user_mapping_provider.config.display_name_template':
|
||||
Failed to parse config for module 'JinjaOidcMappingProvider':
|
||||
invalid jinja template:
|
||||
unexpected end of template, expected 'end of print statement'.
|
||||
|
||||
Args:
|
||||
e: the error to be formatted
|
||||
|
||||
Returns: An iterator which yields string fragments to be formatted
|
||||
"""
|
||||
yield "Error in configuration"
|
||||
|
||||
if e.path:
|
||||
yield " at '%s'" % (".".join(e.path),)
|
||||
|
||||
yield ":\n %s" % (e.msg,)
|
||||
|
||||
parent_e = e.__cause__
|
||||
indent = 1
|
||||
while parent_e:
|
||||
indent += 1
|
||||
yield ":\n%s%s" % (" " * indent, str(parent_e))
|
||||
parent_e = parent_e.__cause__
|
||||
|
||||
|
||||
def run(hs: HomeServer) -> None:
|
||||
_base.start_reactor(
|
||||
"synapse-homeserver",
|
||||
|
||||
@@ -16,14 +16,18 @@
|
||||
|
||||
import argparse
|
||||
import errno
|
||||
import logging
|
||||
import os
|
||||
from collections import OrderedDict
|
||||
from hashlib import sha256
|
||||
from textwrap import dedent
|
||||
from typing import (
|
||||
Any,
|
||||
ClassVar,
|
||||
Collection,
|
||||
Dict,
|
||||
Iterable,
|
||||
Iterator,
|
||||
List,
|
||||
MutableMapping,
|
||||
Optional,
|
||||
@@ -40,6 +44,8 @@ import yaml
|
||||
|
||||
from synapse.util.templates import _create_mxc_to_http_filter, _format_ts_filter
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class ConfigError(Exception):
|
||||
"""Represents a problem parsing the configuration
|
||||
@@ -55,6 +61,38 @@ class ConfigError(Exception):
|
||||
self.path = path
|
||||
|
||||
|
||||
def format_config_error(e: ConfigError) -> Iterator[str]:
|
||||
"""
|
||||
Formats a config error neatly
|
||||
|
||||
The idea is to format the immediate error, plus the "causes" of those errors,
|
||||
hopefully in a way that makes sense to the user. For example:
|
||||
|
||||
Error in configuration at 'oidc_config.user_mapping_provider.config.display_name_template':
|
||||
Failed to parse config for module 'JinjaOidcMappingProvider':
|
||||
invalid jinja template:
|
||||
unexpected end of template, expected 'end of print statement'.
|
||||
|
||||
Args:
|
||||
e: the error to be formatted
|
||||
|
||||
Returns: An iterator which yields string fragments to be formatted
|
||||
"""
|
||||
yield "Error in configuration"
|
||||
|
||||
if e.path:
|
||||
yield " at '%s'" % (".".join(e.path),)
|
||||
|
||||
yield ":\n %s" % (e.msg,)
|
||||
|
||||
parent_e = e.__cause__
|
||||
indent = 1
|
||||
while parent_e:
|
||||
indent += 1
|
||||
yield ":\n%s%s" % (" " * indent, str(parent_e))
|
||||
parent_e = parent_e.__cause__
|
||||
|
||||
|
||||
# We split these messages out to allow packages to override with package
|
||||
# specific instructions.
|
||||
MISSING_REPORT_STATS_CONFIG_INSTRUCTIONS = """\
|
||||
@@ -119,7 +157,7 @@ class Config:
|
||||
defined in subclasses.
|
||||
"""
|
||||
|
||||
section: str
|
||||
section: ClassVar[str]
|
||||
|
||||
def __init__(self, root_config: "RootConfig" = None):
|
||||
self.root = root_config
|
||||
@@ -309,9 +347,12 @@ class RootConfig:
|
||||
class, lower-cased and with "Config" removed.
|
||||
"""
|
||||
|
||||
config_classes = []
|
||||
config_classes: List[Type[Config]] = []
|
||||
|
||||
def __init__(self, config_files: Collection[str] = ()):
|
||||
# Capture absolute paths here, so we can reload config after we daemonize.
|
||||
self.config_files = [os.path.abspath(path) for path in config_files]
|
||||
|
||||
def __init__(self):
|
||||
for config_class in self.config_classes:
|
||||
if config_class.section is None:
|
||||
raise ValueError("%r requires a section name" % (config_class,))
|
||||
@@ -512,12 +553,10 @@ class RootConfig:
|
||||
object from parser.parse_args(..)`
|
||||
"""
|
||||
|
||||
obj = cls()
|
||||
|
||||
config_args = parser.parse_args(argv)
|
||||
|
||||
config_files = find_config_files(search_paths=config_args.config_path)
|
||||
|
||||
obj = cls(config_files)
|
||||
if not config_files:
|
||||
parser.error("Must supply a config file.")
|
||||
|
||||
@@ -627,7 +666,7 @@ class RootConfig:
|
||||
|
||||
generate_missing_configs = config_args.generate_missing_configs
|
||||
|
||||
obj = cls()
|
||||
obj = cls(config_files)
|
||||
|
||||
if config_args.generate_config:
|
||||
if config_args.report_stats is None:
|
||||
@@ -727,6 +766,34 @@ class RootConfig:
|
||||
) -> None:
|
||||
self.invoke_all("generate_files", config_dict, config_dir_path)
|
||||
|
||||
def reload_config_section(self, section_name: str) -> Config:
|
||||
"""Reconstruct the given config section, leaving all others unchanged.
|
||||
|
||||
This works in three steps:
|
||||
|
||||
1. Create a new instance of the relevant `Config` subclass.
|
||||
2. Call `read_config` on that instance to parse the new config.
|
||||
3. Replace the existing config instance with the new one.
|
||||
|
||||
:raises ValueError: if the given `section` does not exist.
|
||||
:raises ConfigError: for any other problems reloading config.
|
||||
|
||||
:returns: the previous config object, which no longer has a reference to this
|
||||
RootConfig.
|
||||
"""
|
||||
existing_config: Optional[Config] = getattr(self, section_name, None)
|
||||
if existing_config is None:
|
||||
raise ValueError(f"Unknown config section '{section_name}'")
|
||||
logger.info("Reloading config section '%s'", section_name)
|
||||
|
||||
new_config_data = read_config_files(self.config_files)
|
||||
new_config = type(existing_config)(self)
|
||||
new_config.read_config(new_config_data)
|
||||
setattr(self, section_name, new_config)
|
||||
|
||||
existing_config.root = None
|
||||
return existing_config
|
||||
|
||||
|
||||
def read_config_files(config_files: Iterable[str]) -> Dict[str, Any]:
|
||||
"""Read the config files into a dict
|
||||
|
||||
@@ -1,15 +1,19 @@
|
||||
import argparse
|
||||
from typing import (
|
||||
Any,
|
||||
Collection,
|
||||
Dict,
|
||||
Iterable,
|
||||
Iterator,
|
||||
List,
|
||||
Literal,
|
||||
MutableMapping,
|
||||
Optional,
|
||||
Tuple,
|
||||
Type,
|
||||
TypeVar,
|
||||
Union,
|
||||
overload,
|
||||
)
|
||||
|
||||
import jinja2
|
||||
@@ -64,6 +68,8 @@ class ConfigError(Exception):
|
||||
self.msg = msg
|
||||
self.path = path
|
||||
|
||||
def format_config_error(e: ConfigError) -> Iterator[str]: ...
|
||||
|
||||
MISSING_REPORT_STATS_CONFIG_INSTRUCTIONS: str
|
||||
MISSING_REPORT_STATS_SPIEL: str
|
||||
MISSING_SERVER_NAME: str
|
||||
@@ -117,7 +123,8 @@ class RootConfig:
|
||||
background_updates: background_updates.BackgroundUpdateConfig
|
||||
|
||||
config_classes: List[Type["Config"]] = ...
|
||||
def __init__(self) -> None: ...
|
||||
config_files: List[str]
|
||||
def __init__(self, config_files: Collection[str] = ...) -> None: ...
|
||||
def invoke_all(
|
||||
self, func_name: str, *args: Any, **kwargs: Any
|
||||
) -> MutableMapping[str, Any]: ...
|
||||
@@ -157,6 +164,12 @@ class RootConfig:
|
||||
def generate_missing_files(
|
||||
self, config_dict: dict, config_dir_path: str
|
||||
) -> None: ...
|
||||
@overload
|
||||
def reload_config_section(
|
||||
self, section_name: Literal["caches"]
|
||||
) -> cache.CacheConfig: ...
|
||||
@overload
|
||||
def reload_config_section(self, section_name: str) -> Config: ...
|
||||
|
||||
class Config:
|
||||
root: RootConfig
|
||||
|
||||
@@ -69,11 +69,11 @@ def _canonicalise_cache_name(cache_name: str) -> str:
|
||||
def add_resizable_cache(
|
||||
cache_name: str, cache_resize_callback: Callable[[float], None]
|
||||
) -> None:
|
||||
"""Register a cache that's size can dynamically change
|
||||
"""Register a cache whose size can dynamically change
|
||||
|
||||
Args:
|
||||
cache_name: A reference to the cache
|
||||
cache_resize_callback: A callback function that will be ran whenever
|
||||
cache_resize_callback: A callback function that will run whenever
|
||||
the cache needs to be resized
|
||||
"""
|
||||
# Some caches have '*' in them which we strip out.
|
||||
@@ -96,6 +96,13 @@ class CacheConfig(Config):
|
||||
section = "caches"
|
||||
_environ = os.environ
|
||||
|
||||
event_cache_size: int
|
||||
cache_factors: Dict[str, float]
|
||||
global_factor: float
|
||||
track_memory_usage: bool
|
||||
expiry_time_msec: Optional[int]
|
||||
sync_response_cache_duration: int
|
||||
|
||||
@staticmethod
|
||||
def reset() -> None:
|
||||
"""Resets the caches to their defaults. Used for tests."""
|
||||
@@ -115,6 +122,12 @@ class CacheConfig(Config):
|
||||
# A cache 'factor' is a multiplier that can be applied to each of
|
||||
# Synapse's caches in order to increase or decrease the maximum
|
||||
# number of entries that can be stored.
|
||||
#
|
||||
# The configuration for cache factors (caches.global_factor and
|
||||
# caches.per_cache_factors) can be reloaded while the application is running,
|
||||
# by sending a SIGHUP signal to the Synapse process. Changes to other parts of
|
||||
# the caching config will NOT be applied after a SIGHUP is received; a restart
|
||||
# is necessary.
|
||||
|
||||
# The number of events to cache in memory. Not affected by
|
||||
# caches.global_factor.
|
||||
@@ -163,6 +176,24 @@ class CacheConfig(Config):
|
||||
#
|
||||
#cache_entry_ttl: 30m
|
||||
|
||||
# This flag enables cache autotuning, and is further specified by the sub-options `max_cache_memory_usage`,
|
||||
# `target_cache_memory_usage`, `min_cache_ttl`. These flags work in conjunction with each other to maintain
|
||||
# a balance between cache memory usage and cache entry availability. You must be using jemalloc to utilize
|
||||
# this option, and all three of the options must be specified for this feature to work.
|
||||
#cache_autotuning:
|
||||
# This flag sets a ceiling on 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
|
||||
# the flag below, or until the `min_cache_ttl` is hit.
|
||||
#max_cache_memory_usage: 1024M
|
||||
|
||||
# This flag sets a rough target for the desired memory usage of the caches.
|
||||
#target_cache_memory_usage: 758M
|
||||
|
||||
# 'min_cache_ttl` sets a limit under which newer cache entries are not evicted and is only applied when
|
||||
# caches are actively being evicted/`max_cache_memory_usage` has been exceeded. This is to protect hot caches
|
||||
# from being emptied while Synapse is evicting due to memory.
|
||||
#min_cache_ttl: 5m
|
||||
|
||||
# Controls how long the results of a /sync request are cached for after
|
||||
# a successful response is returned. A higher duration can help clients with
|
||||
# intermittent connections, at the cost of higher memory usage.
|
||||
@@ -174,21 +205,21 @@ class CacheConfig(Config):
|
||||
"""
|
||||
|
||||
def read_config(self, config: JsonDict, **kwargs: Any) -> None:
|
||||
"""Populate this config object with values from `config`.
|
||||
|
||||
This method does NOT resize existing or future caches: use `resize_all_caches`.
|
||||
We use two separate methods so that we can reject bad config before applying it.
|
||||
"""
|
||||
self.event_cache_size = self.parse_size(
|
||||
config.get("event_cache_size", _DEFAULT_EVENT_CACHE_SIZE)
|
||||
)
|
||||
self.cache_factors: Dict[str, float] = {}
|
||||
self.cache_factors = {}
|
||||
|
||||
cache_config = config.get("caches") or {}
|
||||
self.global_factor = cache_config.get(
|
||||
"global_factor", properties.default_factor_size
|
||||
)
|
||||
self.global_factor = cache_config.get("global_factor", _DEFAULT_FACTOR_SIZE)
|
||||
if not isinstance(self.global_factor, (int, float)):
|
||||
raise ConfigError("caches.global_factor must be a number.")
|
||||
|
||||
# Set the global one so that it's reflected in new caches
|
||||
properties.default_factor_size = self.global_factor
|
||||
|
||||
# Load cache factors from the config
|
||||
individual_factors = cache_config.get("per_cache_factors") or {}
|
||||
if not isinstance(individual_factors, dict):
|
||||
@@ -230,7 +261,7 @@ class CacheConfig(Config):
|
||||
cache_entry_ttl = cache_config.get("cache_entry_ttl", "30m")
|
||||
|
||||
if expire_caches:
|
||||
self.expiry_time_msec: Optional[int] = self.parse_duration(cache_entry_ttl)
|
||||
self.expiry_time_msec = self.parse_duration(cache_entry_ttl)
|
||||
else:
|
||||
self.expiry_time_msec = None
|
||||
|
||||
@@ -250,23 +281,38 @@ class CacheConfig(Config):
|
||||
)
|
||||
self.expiry_time_msec = self.parse_duration(expiry_time)
|
||||
|
||||
self.cache_autotuning = cache_config.get("cache_autotuning")
|
||||
if self.cache_autotuning:
|
||||
max_memory_usage = self.cache_autotuning.get("max_cache_memory_usage")
|
||||
self.cache_autotuning["max_cache_memory_usage"] = self.parse_size(
|
||||
max_memory_usage
|
||||
)
|
||||
|
||||
target_mem_size = self.cache_autotuning.get("target_cache_memory_usage")
|
||||
self.cache_autotuning["target_cache_memory_usage"] = self.parse_size(
|
||||
target_mem_size
|
||||
)
|
||||
|
||||
min_cache_ttl = self.cache_autotuning.get("min_cache_ttl")
|
||||
self.cache_autotuning["min_cache_ttl"] = self.parse_duration(min_cache_ttl)
|
||||
|
||||
self.sync_response_cache_duration = self.parse_duration(
|
||||
cache_config.get("sync_response_cache_duration", 0)
|
||||
)
|
||||
|
||||
# Resize all caches (if necessary) with the new factors we've loaded
|
||||
self.resize_all_caches()
|
||||
|
||||
# Store this function so that it can be called from other classes without
|
||||
# needing an instance of Config
|
||||
properties.resize_all_caches_func = self.resize_all_caches
|
||||
|
||||
def resize_all_caches(self) -> None:
|
||||
"""Ensure all cache sizes are up to date
|
||||
"""Ensure all cache sizes are up-to-date.
|
||||
|
||||
For each cache, run the mapped callback function with either
|
||||
a specific cache factor or the default, global one.
|
||||
"""
|
||||
# Set the global factor size, so that new caches are appropriately sized.
|
||||
properties.default_factor_size = self.global_factor
|
||||
|
||||
# Store this function so that it can be called from other classes without
|
||||
# needing an instance of CacheConfig
|
||||
properties.resize_all_caches_func = self.resize_all_caches
|
||||
|
||||
# block other threads from modifying _CACHES while we iterate it.
|
||||
with _CACHES_LOCK:
|
||||
for cache_name, callback in _CACHES.items():
|
||||
|
||||
@@ -63,6 +63,19 @@ class RoomConfig(Config):
|
||||
"Invalid value for encryption_enabled_by_default_for_room_type"
|
||||
)
|
||||
|
||||
self.default_power_level_content_override = config.get(
|
||||
"default_power_level_content_override",
|
||||
None,
|
||||
)
|
||||
if self.default_power_level_content_override is not None:
|
||||
for preset in self.default_power_level_content_override:
|
||||
if preset not in vars(RoomCreationPreset).values():
|
||||
raise ConfigError(
|
||||
"Unrecognised room preset %s in default_power_level_content_override"
|
||||
% preset
|
||||
)
|
||||
# We validate the actual overrides when we try to apply them.
|
||||
|
||||
def generate_config_section(self, **kwargs: Any) -> str:
|
||||
return """\
|
||||
## Rooms ##
|
||||
@@ -83,4 +96,38 @@ class RoomConfig(Config):
|
||||
# will also not affect rooms created by other servers.
|
||||
#
|
||||
#encryption_enabled_by_default_for_room_type: invite
|
||||
|
||||
# Override the default power levels for rooms created on this server, per
|
||||
# room creation preset.
|
||||
#
|
||||
# The appropriate dictionary for the room preset will be applied on top
|
||||
# of the existing power levels content.
|
||||
#
|
||||
# Useful if you know that your users need special permissions in rooms
|
||||
# that they create (e.g. to send particular types of state events without
|
||||
# needing an elevated power level). This takes the same shape as the
|
||||
# `power_level_content_override` parameter in the /createRoom API, but
|
||||
# is applied before that parameter.
|
||||
#
|
||||
# Valid keys are some or all of `private_chat`, `trusted_private_chat`
|
||||
# and `public_chat`. Inside each of those should be any of the
|
||||
# properties allowed in `power_level_content_override` in the
|
||||
# /createRoom API. If any property is missing, its default value will
|
||||
# continue to be used. If any property is present, it will overwrite
|
||||
# the existing default completely (so if the `events` property exists,
|
||||
# the default event power levels will be ignored).
|
||||
#
|
||||
#default_power_level_content_override:
|
||||
# private_chat:
|
||||
# "events":
|
||||
# "com.example.myeventtype" : 0
|
||||
# "m.room.avatar": 50
|
||||
# "m.room.canonical_alias": 50
|
||||
# "m.room.encryption": 100
|
||||
# "m.room.history_visibility": 100
|
||||
# "m.room.name": 50
|
||||
# "m.room.power_levels": 100
|
||||
# "m.room.server_acl": 100
|
||||
# "m.room.tombstone": 100
|
||||
# "events_default": 1
|
||||
"""
|
||||
|
||||
@@ -996,7 +996,7 @@ class ServerConfig(Config):
|
||||
# federation: the server-server API (/_matrix/federation). Also implies
|
||||
# 'media', 'keys', 'openid'
|
||||
#
|
||||
# keys: the key discovery API (/_matrix/keys).
|
||||
# keys: the key discovery API (/_matrix/key).
|
||||
#
|
||||
# media: the media API (/_matrix/media).
|
||||
#
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
# limitations under the License.
|
||||
|
||||
import abc
|
||||
import collections.abc
|
||||
import os
|
||||
from typing import (
|
||||
TYPE_CHECKING,
|
||||
@@ -32,9 +33,11 @@ from typing import (
|
||||
overload,
|
||||
)
|
||||
|
||||
import attr
|
||||
from typing_extensions import Literal
|
||||
from unpaddedbase64 import encode_base64
|
||||
|
||||
from synapse.api.constants import RelationTypes
|
||||
from synapse.api.room_versions import EventFormatVersions, RoomVersion, RoomVersions
|
||||
from synapse.types import JsonDict, RoomStreamToken
|
||||
from synapse.util.caches import intern_dict
|
||||
@@ -615,3 +618,45 @@ def make_event_from_dict(
|
||||
return event_type(
|
||||
event_dict, room_version, internal_metadata_dict or {}, rejected_reason
|
||||
)
|
||||
|
||||
|
||||
@attr.s(slots=True, frozen=True, auto_attribs=True)
|
||||
class _EventRelation:
|
||||
# The target event of the relation.
|
||||
parent_id: str
|
||||
# The relation type.
|
||||
rel_type: str
|
||||
# The aggregation key. Will be None if the rel_type is not m.annotation or is
|
||||
# not a string.
|
||||
aggregation_key: Optional[str]
|
||||
|
||||
|
||||
def relation_from_event(event: EventBase) -> Optional[_EventRelation]:
|
||||
"""
|
||||
Attempt to parse relation information an event.
|
||||
|
||||
Returns:
|
||||
The event relation information, if it is valid. None, otherwise.
|
||||
"""
|
||||
relation = event.content.get("m.relates_to")
|
||||
if not relation or not isinstance(relation, collections.abc.Mapping):
|
||||
# No relation information.
|
||||
return None
|
||||
|
||||
# Relations must have a type and parent event ID.
|
||||
rel_type = relation.get("rel_type")
|
||||
if not isinstance(rel_type, str):
|
||||
return None
|
||||
|
||||
parent_id = relation.get("event_id")
|
||||
if not isinstance(parent_id, str):
|
||||
return None
|
||||
|
||||
# Annotations have a key field.
|
||||
aggregation_key = None
|
||||
if rel_type == RelationTypes.ANNOTATION:
|
||||
aggregation_key = relation.get("key")
|
||||
if not isinstance(aggregation_key, str):
|
||||
aggregation_key = None
|
||||
|
||||
return _EventRelation(parent_id, rel_type, aggregation_key)
|
||||
|
||||
@@ -15,12 +15,10 @@ from typing import TYPE_CHECKING, List, Optional, Tuple, Union
|
||||
|
||||
import attr
|
||||
from frozendict import frozendict
|
||||
|
||||
from twisted.internet.defer import Deferred
|
||||
from typing_extensions import Literal
|
||||
|
||||
from synapse.appservice import ApplicationService
|
||||
from synapse.events import EventBase
|
||||
from synapse.logging.context import make_deferred_yieldable, run_in_background
|
||||
from synapse.types import JsonDict, StateMap
|
||||
|
||||
if TYPE_CHECKING:
|
||||
@@ -60,6 +58,9 @@ class EventContext:
|
||||
If ``state_group`` is None (ie, the event is an outlier),
|
||||
``state_group_before_event`` will always also be ``None``.
|
||||
|
||||
state_delta_due_to_event: If `state_group` and `state_group_before_event` are not None
|
||||
then this is the delta of the state between the two groups.
|
||||
|
||||
prev_group: If it is known, ``state_group``'s prev_group. Note that this being
|
||||
None does not necessarily mean that ``state_group`` does not have
|
||||
a prev_group!
|
||||
@@ -78,73 +79,47 @@ class EventContext:
|
||||
app_service: If this event is being sent by a (local) application service, that
|
||||
app service.
|
||||
|
||||
_current_state_ids: The room state map, including this event - ie, the state
|
||||
in ``state_group``.
|
||||
|
||||
(type, state_key) -> event_id
|
||||
|
||||
For an outlier, this is {}
|
||||
|
||||
Note that this is a private attribute: it should be accessed via
|
||||
``get_current_state_ids``. _AsyncEventContext impl calculates this
|
||||
on-demand: it will be None until that happens.
|
||||
|
||||
_prev_state_ids: The room state map, excluding this event - ie, the state
|
||||
in ``state_group_before_event``. For a non-state
|
||||
event, this will be the same as _current_state_events.
|
||||
|
||||
Note that it is a completely different thing to prev_group!
|
||||
|
||||
(type, state_key) -> event_id
|
||||
|
||||
For an outlier, this is {}
|
||||
|
||||
As with _current_state_ids, this is a private attribute. It should be
|
||||
accessed via get_prev_state_ids.
|
||||
|
||||
partial_state: if True, we may be storing this event with a temporary,
|
||||
incomplete state.
|
||||
"""
|
||||
|
||||
rejected: Union[bool, str] = False
|
||||
_storage: "Storage"
|
||||
rejected: Union[Literal[False], str] = False
|
||||
_state_group: Optional[int] = None
|
||||
state_group_before_event: Optional[int] = None
|
||||
_state_delta_due_to_event: Optional[StateMap[str]] = None
|
||||
prev_group: Optional[int] = None
|
||||
delta_ids: Optional[StateMap[str]] = None
|
||||
app_service: Optional[ApplicationService] = None
|
||||
|
||||
_current_state_ids: Optional[StateMap[str]] = None
|
||||
_prev_state_ids: Optional[StateMap[str]] = None
|
||||
|
||||
partial_state: bool = False
|
||||
|
||||
@staticmethod
|
||||
def with_state(
|
||||
storage: "Storage",
|
||||
state_group: Optional[int],
|
||||
state_group_before_event: Optional[int],
|
||||
current_state_ids: Optional[StateMap[str]],
|
||||
prev_state_ids: Optional[StateMap[str]],
|
||||
state_delta_due_to_event: Optional[StateMap[str]],
|
||||
partial_state: bool,
|
||||
prev_group: Optional[int] = None,
|
||||
delta_ids: Optional[StateMap[str]] = None,
|
||||
) -> "EventContext":
|
||||
return EventContext(
|
||||
current_state_ids=current_state_ids,
|
||||
prev_state_ids=prev_state_ids,
|
||||
storage=storage,
|
||||
state_group=state_group,
|
||||
state_group_before_event=state_group_before_event,
|
||||
state_delta_due_to_event=state_delta_due_to_event,
|
||||
prev_group=prev_group,
|
||||
delta_ids=delta_ids,
|
||||
partial_state=partial_state,
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def for_outlier() -> "EventContext":
|
||||
def for_outlier(
|
||||
storage: "Storage",
|
||||
) -> "EventContext":
|
||||
"""Return an EventContext instance suitable for persisting an outlier event"""
|
||||
return EventContext(
|
||||
current_state_ids={},
|
||||
prev_state_ids={},
|
||||
)
|
||||
return EventContext(storage=storage)
|
||||
|
||||
async def serialize(self, event: EventBase, store: "DataStore") -> JsonDict:
|
||||
"""Converts self to a type that can be serialized as JSON, and then
|
||||
@@ -157,24 +132,14 @@ class EventContext:
|
||||
The serialized event.
|
||||
"""
|
||||
|
||||
# We don't serialize the full state dicts, instead they get pulled out
|
||||
# of the DB on the other side. However, the other side can't figure out
|
||||
# the prev_state_ids, so if we're a state event we include the event
|
||||
# id that we replaced in the state.
|
||||
if event.is_state():
|
||||
prev_state_ids = await self.get_prev_state_ids()
|
||||
prev_state_id = prev_state_ids.get((event.type, event.state_key))
|
||||
else:
|
||||
prev_state_id = None
|
||||
|
||||
return {
|
||||
"prev_state_id": prev_state_id,
|
||||
"event_type": event.type,
|
||||
"event_state_key": event.get_state_key(),
|
||||
"state_group": self._state_group,
|
||||
"state_group_before_event": self.state_group_before_event,
|
||||
"rejected": self.rejected,
|
||||
"prev_group": self.prev_group,
|
||||
"state_delta_due_to_event": _encode_state_dict(
|
||||
self._state_delta_due_to_event
|
||||
),
|
||||
"delta_ids": _encode_state_dict(self.delta_ids),
|
||||
"app_service_id": self.app_service.id if self.app_service else None,
|
||||
"partial_state": self.partial_state,
|
||||
@@ -192,16 +157,16 @@ class EventContext:
|
||||
Returns:
|
||||
The event context.
|
||||
"""
|
||||
context = _AsyncEventContextImpl(
|
||||
context = EventContext(
|
||||
# We use the state_group and prev_state_id stuff to pull the
|
||||
# current_state_ids out of the DB and construct prev_state_ids.
|
||||
storage=storage,
|
||||
prev_state_id=input["prev_state_id"],
|
||||
event_type=input["event_type"],
|
||||
event_state_key=input["event_state_key"],
|
||||
state_group=input["state_group"],
|
||||
state_group_before_event=input["state_group_before_event"],
|
||||
prev_group=input["prev_group"],
|
||||
state_delta_due_to_event=_decode_state_dict(
|
||||
input["state_delta_due_to_event"]
|
||||
),
|
||||
delta_ids=_decode_state_dict(input["delta_ids"]),
|
||||
rejected=input["rejected"],
|
||||
partial_state=input.get("partial_state", False),
|
||||
@@ -249,8 +214,15 @@ class EventContext:
|
||||
if self.rejected:
|
||||
raise RuntimeError("Attempt to access state_ids of rejected event")
|
||||
|
||||
await self._ensure_fetched()
|
||||
return self._current_state_ids
|
||||
assert self._state_delta_due_to_event is not None
|
||||
|
||||
prev_state_ids = await self.get_prev_state_ids()
|
||||
|
||||
if self._state_delta_due_to_event:
|
||||
prev_state_ids = dict(prev_state_ids)
|
||||
prev_state_ids.update(self._state_delta_due_to_event)
|
||||
|
||||
return prev_state_ids
|
||||
|
||||
async def get_prev_state_ids(self) -> StateMap[str]:
|
||||
"""
|
||||
@@ -265,94 +237,10 @@ class EventContext:
|
||||
Maps a (type, state_key) to the event ID of the state event matching
|
||||
this tuple.
|
||||
"""
|
||||
await self._ensure_fetched()
|
||||
# There *should* be previous state IDs now.
|
||||
assert self._prev_state_ids is not None
|
||||
return self._prev_state_ids
|
||||
|
||||
def get_cached_current_state_ids(self) -> Optional[StateMap[str]]:
|
||||
"""Gets the current state IDs if we have them already cached.
|
||||
|
||||
It is an error to access this for a rejected event, since rejected state should
|
||||
not make it into the room state. This method will raise an exception if
|
||||
``rejected`` is set.
|
||||
|
||||
Returns:
|
||||
Returns None if we haven't cached the state or if state_group is None
|
||||
(which happens when the associated event is an outlier).
|
||||
|
||||
Otherwise, returns the the current state IDs.
|
||||
"""
|
||||
if self.rejected:
|
||||
raise RuntimeError("Attempt to access state_ids of rejected event")
|
||||
|
||||
return self._current_state_ids
|
||||
|
||||
async def _ensure_fetched(self) -> None:
|
||||
return None
|
||||
|
||||
|
||||
@attr.s(slots=True)
|
||||
class _AsyncEventContextImpl(EventContext):
|
||||
"""
|
||||
An implementation of EventContext which fetches _current_state_ids and
|
||||
_prev_state_ids from the database on demand.
|
||||
|
||||
Attributes:
|
||||
|
||||
_storage
|
||||
|
||||
_fetching_state_deferred: Resolves when *_state_ids have been calculated.
|
||||
None if we haven't started calculating yet
|
||||
|
||||
_event_type: The type of the event the context is associated with.
|
||||
|
||||
_event_state_key: The state_key of the event the context is associated with.
|
||||
|
||||
_prev_state_id: If the event associated with the context is a state event,
|
||||
then `_prev_state_id` is the event_id of the state that was replaced.
|
||||
"""
|
||||
|
||||
# This needs to have a default as we're inheriting
|
||||
_storage: "Storage" = attr.ib(default=None)
|
||||
_prev_state_id: Optional[str] = attr.ib(default=None)
|
||||
_event_type: str = attr.ib(default=None)
|
||||
_event_state_key: Optional[str] = attr.ib(default=None)
|
||||
_fetching_state_deferred: Optional["Deferred[None]"] = attr.ib(default=None)
|
||||
|
||||
async def _ensure_fetched(self) -> None:
|
||||
if not self._fetching_state_deferred:
|
||||
self._fetching_state_deferred = run_in_background(self._fill_out_state)
|
||||
|
||||
await make_deferred_yieldable(self._fetching_state_deferred)
|
||||
|
||||
async def _fill_out_state(self) -> None:
|
||||
"""Called to populate the _current_state_ids and _prev_state_ids
|
||||
attributes by loading from the database.
|
||||
"""
|
||||
if self.state_group is None:
|
||||
# No state group means the event is an outlier. Usually the state_ids dicts are also
|
||||
# pre-set to empty dicts, but they get reset when the context is serialized, so set
|
||||
# them to empty dicts again here.
|
||||
self._current_state_ids = {}
|
||||
self._prev_state_ids = {}
|
||||
return
|
||||
|
||||
current_state_ids = await self._storage.state.get_state_ids_for_group(
|
||||
self.state_group
|
||||
assert self.state_group_before_event is not None
|
||||
return await self._storage.state.get_state_ids_for_group(
|
||||
self.state_group_before_event
|
||||
)
|
||||
# Set this separately so mypy knows current_state_ids is not None.
|
||||
self._current_state_ids = current_state_ids
|
||||
if self._event_state_key is not None:
|
||||
self._prev_state_ids = dict(current_state_ids)
|
||||
|
||||
key = (self._event_type, self._event_state_key)
|
||||
if self._prev_state_id:
|
||||
self._prev_state_ids[key] = self._prev_state_id
|
||||
else:
|
||||
self._prev_state_ids.pop(key, None)
|
||||
else:
|
||||
self._prev_state_ids = current_state_ids
|
||||
|
||||
|
||||
def _encode_state_dict(
|
||||
|
||||
@@ -32,6 +32,7 @@ from synapse.rest.media.v1.media_storage import ReadableFileWrapper
|
||||
from synapse.spam_checker_api import RegistrationBehaviour
|
||||
from synapse.types import RoomAlias, UserProfile
|
||||
from synapse.util.async_helpers import delay_cancellation, maybe_awaitable
|
||||
from synapse.util.metrics import Measure
|
||||
|
||||
if TYPE_CHECKING:
|
||||
import synapse.events
|
||||
@@ -162,7 +163,10 @@ def load_legacy_spam_checkers(hs: "synapse.server.HomeServer") -> None:
|
||||
|
||||
|
||||
class SpamChecker:
|
||||
def __init__(self) -> None:
|
||||
def __init__(self, hs: "synapse.server.HomeServer") -> None:
|
||||
self.hs = hs
|
||||
self.clock = hs.get_clock()
|
||||
|
||||
self._check_event_for_spam_callbacks: List[CHECK_EVENT_FOR_SPAM_CALLBACK] = []
|
||||
self._user_may_join_room_callbacks: List[USER_MAY_JOIN_ROOM_CALLBACK] = []
|
||||
self._user_may_invite_callbacks: List[USER_MAY_INVITE_CALLBACK] = []
|
||||
@@ -255,7 +259,10 @@ class SpamChecker:
|
||||
will be used as the error message returned to the user.
|
||||
"""
|
||||
for callback in self._check_event_for_spam_callbacks:
|
||||
res: Union[bool, str] = await delay_cancellation(callback(event))
|
||||
with Measure(
|
||||
self.clock, "{}.{}".format(callback.__module__, callback.__qualname__)
|
||||
):
|
||||
res: Union[bool, str] = await delay_cancellation(callback(event))
|
||||
if res:
|
||||
return res
|
||||
|
||||
@@ -276,9 +283,12 @@ class SpamChecker:
|
||||
Whether the user may join the room
|
||||
"""
|
||||
for callback in self._user_may_join_room_callbacks:
|
||||
may_join_room = await delay_cancellation(
|
||||
callback(user_id, room_id, is_invited)
|
||||
)
|
||||
with Measure(
|
||||
self.clock, "{}.{}".format(callback.__module__, callback.__qualname__)
|
||||
):
|
||||
may_join_room = await delay_cancellation(
|
||||
callback(user_id, room_id, is_invited)
|
||||
)
|
||||
if may_join_room is False:
|
||||
return False
|
||||
|
||||
@@ -300,9 +310,12 @@ class SpamChecker:
|
||||
True if the user may send an invite, otherwise False
|
||||
"""
|
||||
for callback in self._user_may_invite_callbacks:
|
||||
may_invite = await delay_cancellation(
|
||||
callback(inviter_userid, invitee_userid, room_id)
|
||||
)
|
||||
with Measure(
|
||||
self.clock, "{}.{}".format(callback.__module__, callback.__qualname__)
|
||||
):
|
||||
may_invite = await delay_cancellation(
|
||||
callback(inviter_userid, invitee_userid, room_id)
|
||||
)
|
||||
if may_invite is False:
|
||||
return False
|
||||
|
||||
@@ -328,9 +341,12 @@ class SpamChecker:
|
||||
True if the user may send the invite, otherwise False
|
||||
"""
|
||||
for callback in self._user_may_send_3pid_invite_callbacks:
|
||||
may_send_3pid_invite = await delay_cancellation(
|
||||
callback(inviter_userid, medium, address, room_id)
|
||||
)
|
||||
with Measure(
|
||||
self.clock, "{}.{}".format(callback.__module__, callback.__qualname__)
|
||||
):
|
||||
may_send_3pid_invite = await delay_cancellation(
|
||||
callback(inviter_userid, medium, address, room_id)
|
||||
)
|
||||
if may_send_3pid_invite is False:
|
||||
return False
|
||||
|
||||
@@ -348,7 +364,10 @@ class SpamChecker:
|
||||
True if the user may create a room, otherwise False
|
||||
"""
|
||||
for callback in self._user_may_create_room_callbacks:
|
||||
may_create_room = await delay_cancellation(callback(userid))
|
||||
with Measure(
|
||||
self.clock, "{}.{}".format(callback.__module__, callback.__qualname__)
|
||||
):
|
||||
may_create_room = await delay_cancellation(callback(userid))
|
||||
if may_create_room is False:
|
||||
return False
|
||||
|
||||
@@ -369,9 +388,12 @@ class SpamChecker:
|
||||
True if the user may create a room alias, otherwise False
|
||||
"""
|
||||
for callback in self._user_may_create_room_alias_callbacks:
|
||||
may_create_room_alias = await delay_cancellation(
|
||||
callback(userid, room_alias)
|
||||
)
|
||||
with Measure(
|
||||
self.clock, "{}.{}".format(callback.__module__, callback.__qualname__)
|
||||
):
|
||||
may_create_room_alias = await delay_cancellation(
|
||||
callback(userid, room_alias)
|
||||
)
|
||||
if may_create_room_alias is False:
|
||||
return False
|
||||
|
||||
@@ -390,7 +412,10 @@ class SpamChecker:
|
||||
True if the user may publish the room, otherwise False
|
||||
"""
|
||||
for callback in self._user_may_publish_room_callbacks:
|
||||
may_publish_room = await delay_cancellation(callback(userid, room_id))
|
||||
with Measure(
|
||||
self.clock, "{}.{}".format(callback.__module__, callback.__qualname__)
|
||||
):
|
||||
may_publish_room = await delay_cancellation(callback(userid, room_id))
|
||||
if may_publish_room is False:
|
||||
return False
|
||||
|
||||
@@ -412,9 +437,13 @@ class SpamChecker:
|
||||
True if the user is spammy.
|
||||
"""
|
||||
for callback in self._check_username_for_spam_callbacks:
|
||||
# Make a copy of the user profile object to ensure the spam checker cannot
|
||||
# modify it.
|
||||
if await delay_cancellation(callback(user_profile.copy())):
|
||||
with Measure(
|
||||
self.clock, "{}.{}".format(callback.__module__, callback.__qualname__)
|
||||
):
|
||||
# Make a copy of the user profile object to ensure the spam checker cannot
|
||||
# modify it.
|
||||
res = await delay_cancellation(callback(user_profile.copy()))
|
||||
if res:
|
||||
return True
|
||||
|
||||
return False
|
||||
@@ -442,9 +471,12 @@ class SpamChecker:
|
||||
"""
|
||||
|
||||
for callback in self._check_registration_for_spam_callbacks:
|
||||
behaviour = await delay_cancellation(
|
||||
callback(email_threepid, username, request_info, auth_provider_id)
|
||||
)
|
||||
with Measure(
|
||||
self.clock, "{}.{}".format(callback.__module__, callback.__qualname__)
|
||||
):
|
||||
behaviour = await delay_cancellation(
|
||||
callback(email_threepid, username, request_info, auth_provider_id)
|
||||
)
|
||||
assert isinstance(behaviour, RegistrationBehaviour)
|
||||
if behaviour != RegistrationBehaviour.ALLOW:
|
||||
return behaviour
|
||||
@@ -486,7 +518,10 @@ class SpamChecker:
|
||||
"""
|
||||
|
||||
for callback in self._check_media_file_for_spam_callbacks:
|
||||
spam = await delay_cancellation(callback(file_wrapper, file_info))
|
||||
with Measure(
|
||||
self.clock, "{}.{}".format(callback.__module__, callback.__qualname__)
|
||||
):
|
||||
spam = await delay_cancellation(callback(file_wrapper, file_info))
|
||||
if spam:
|
||||
return True
|
||||
|
||||
|
||||
@@ -21,7 +21,7 @@ from typing import TYPE_CHECKING, Any, Awaitable, Callable, Dict, Optional, Tupl
|
||||
|
||||
from synapse.api.errors import Codes, FederationDeniedError, SynapseError
|
||||
from synapse.api.urls import FEDERATION_V1_PREFIX
|
||||
from synapse.http.server import HttpServer, ServletCallback
|
||||
from synapse.http.server import HttpServer, ServletCallback, is_method_cancellable
|
||||
from synapse.http.servlet import parse_json_object_from_request
|
||||
from synapse.http.site import SynapseRequest
|
||||
from synapse.logging.context import run_in_background
|
||||
@@ -373,6 +373,17 @@ class BaseFederationServlet:
|
||||
if code is None:
|
||||
continue
|
||||
|
||||
if is_method_cancellable(code):
|
||||
# The wrapper added by `self._wrap` will inherit the cancellable flag,
|
||||
# but the wrapper itself does not support cancellation yet.
|
||||
# Once resolved, the cancellation tests in
|
||||
# `tests/federation/transport/server/test__base.py` can be re-enabled.
|
||||
raise Exception(
|
||||
f"{self.__class__.__name__}.on_{method} has been marked as "
|
||||
"cancellable, but federation servlets do not support cancellation "
|
||||
"yet."
|
||||
)
|
||||
|
||||
server.register_paths(
|
||||
method,
|
||||
(pattern,),
|
||||
|
||||
@@ -934,7 +934,7 @@ class GroupsServerHandler(GroupsServerWorkerHandler):
|
||||
# Before deleting the group lets kick everyone out of it
|
||||
users = await self.store.get_users_in_group(group_id, include_private=True)
|
||||
|
||||
async def _kick_user_from_group(user_id):
|
||||
async def _kick_user_from_group(user_id: str) -> None:
|
||||
if self.hs.is_mine_id(user_id):
|
||||
groups_local = self.hs.get_groups_local_handler()
|
||||
assert isinstance(
|
||||
|
||||
@@ -23,7 +23,7 @@ from synapse.replication.http.account_data import (
|
||||
ReplicationUserAccountDataRestServlet,
|
||||
)
|
||||
from synapse.streams import EventSource
|
||||
from synapse.types import JsonDict, UserID
|
||||
from synapse.types import JsonDict, StreamKeyType, UserID
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from synapse.server import HomeServer
|
||||
@@ -105,7 +105,7 @@ class AccountDataHandler:
|
||||
)
|
||||
|
||||
self._notifier.on_new_event(
|
||||
"account_data_key", max_stream_id, users=[user_id]
|
||||
StreamKeyType.ACCOUNT_DATA, max_stream_id, users=[user_id]
|
||||
)
|
||||
|
||||
await self._notify_modules(user_id, room_id, account_data_type, content)
|
||||
@@ -141,7 +141,7 @@ class AccountDataHandler:
|
||||
)
|
||||
|
||||
self._notifier.on_new_event(
|
||||
"account_data_key", max_stream_id, users=[user_id]
|
||||
StreamKeyType.ACCOUNT_DATA, max_stream_id, users=[user_id]
|
||||
)
|
||||
|
||||
await self._notify_modules(user_id, None, account_data_type, content)
|
||||
@@ -176,7 +176,7 @@ class AccountDataHandler:
|
||||
)
|
||||
|
||||
self._notifier.on_new_event(
|
||||
"account_data_key", max_stream_id, users=[user_id]
|
||||
StreamKeyType.ACCOUNT_DATA, max_stream_id, users=[user_id]
|
||||
)
|
||||
return max_stream_id
|
||||
else:
|
||||
@@ -201,7 +201,7 @@ class AccountDataHandler:
|
||||
)
|
||||
|
||||
self._notifier.on_new_event(
|
||||
"account_data_key", max_stream_id, users=[user_id]
|
||||
StreamKeyType.ACCOUNT_DATA, max_stream_id, users=[user_id]
|
||||
)
|
||||
return max_stream_id
|
||||
else:
|
||||
|
||||
@@ -38,6 +38,7 @@ from synapse.types import (
|
||||
JsonDict,
|
||||
RoomAlias,
|
||||
RoomStreamToken,
|
||||
StreamKeyType,
|
||||
UserID,
|
||||
)
|
||||
from synapse.util.async_helpers import Linearizer
|
||||
@@ -213,8 +214,8 @@ class ApplicationServicesHandler:
|
||||
Args:
|
||||
stream_key: The stream the event came from.
|
||||
|
||||
`stream_key` can be "typing_key", "receipt_key", "presence_key",
|
||||
"to_device_key" or "device_list_key". Any other value for `stream_key`
|
||||
`stream_key` can be StreamKeyType.TYPING, StreamKeyType.RECEIPT, StreamKeyType.PRESENCE,
|
||||
StreamKeyType.TO_DEVICE or StreamKeyType.DEVICE_LIST. Any other value for `stream_key`
|
||||
will cause this function to return early.
|
||||
|
||||
Ephemeral events will only be pushed to appservices that have opted into
|
||||
@@ -235,11 +236,11 @@ class ApplicationServicesHandler:
|
||||
# Only the following streams are currently supported.
|
||||
# FIXME: We should use constants for these values.
|
||||
if stream_key not in (
|
||||
"typing_key",
|
||||
"receipt_key",
|
||||
"presence_key",
|
||||
"to_device_key",
|
||||
"device_list_key",
|
||||
StreamKeyType.TYPING,
|
||||
StreamKeyType.RECEIPT,
|
||||
StreamKeyType.PRESENCE,
|
||||
StreamKeyType.TO_DEVICE,
|
||||
StreamKeyType.DEVICE_LIST,
|
||||
):
|
||||
return
|
||||
|
||||
@@ -258,14 +259,14 @@ class ApplicationServicesHandler:
|
||||
|
||||
# Ignore to-device messages if the feature flag is not enabled
|
||||
if (
|
||||
stream_key == "to_device_key"
|
||||
stream_key == StreamKeyType.TO_DEVICE
|
||||
and not self._msc2409_to_device_messages_enabled
|
||||
):
|
||||
return
|
||||
|
||||
# Ignore device lists if the feature flag is not enabled
|
||||
if (
|
||||
stream_key == "device_list_key"
|
||||
stream_key == StreamKeyType.DEVICE_LIST
|
||||
and not self._msc3202_transaction_extensions_enabled
|
||||
):
|
||||
return
|
||||
@@ -283,15 +284,15 @@ class ApplicationServicesHandler:
|
||||
if (
|
||||
stream_key
|
||||
in (
|
||||
"typing_key",
|
||||
"receipt_key",
|
||||
"presence_key",
|
||||
"to_device_key",
|
||||
StreamKeyType.TYPING,
|
||||
StreamKeyType.RECEIPT,
|
||||
StreamKeyType.PRESENCE,
|
||||
StreamKeyType.TO_DEVICE,
|
||||
)
|
||||
and service.supports_ephemeral
|
||||
)
|
||||
or (
|
||||
stream_key == "device_list_key"
|
||||
stream_key == StreamKeyType.DEVICE_LIST
|
||||
and service.msc3202_transaction_extensions
|
||||
)
|
||||
]
|
||||
@@ -317,7 +318,7 @@ class ApplicationServicesHandler:
|
||||
logger.debug("Checking interested services for %s", stream_key)
|
||||
with Measure(self.clock, "notify_interested_services_ephemeral"):
|
||||
for service in services:
|
||||
if stream_key == "typing_key":
|
||||
if stream_key == StreamKeyType.TYPING:
|
||||
# Note that we don't persist the token (via set_appservice_stream_type_pos)
|
||||
# for typing_key due to performance reasons and due to their highly
|
||||
# ephemeral nature.
|
||||
@@ -333,7 +334,7 @@ class ApplicationServicesHandler:
|
||||
async with self._ephemeral_events_linearizer.queue(
|
||||
(service.id, stream_key)
|
||||
):
|
||||
if stream_key == "receipt_key":
|
||||
if stream_key == StreamKeyType.RECEIPT:
|
||||
events = await self._handle_receipts(service, new_token)
|
||||
self.scheduler.enqueue_for_appservice(service, ephemeral=events)
|
||||
|
||||
@@ -342,7 +343,7 @@ class ApplicationServicesHandler:
|
||||
service, "read_receipt", new_token
|
||||
)
|
||||
|
||||
elif stream_key == "presence_key":
|
||||
elif stream_key == StreamKeyType.PRESENCE:
|
||||
events = await self._handle_presence(service, users, new_token)
|
||||
self.scheduler.enqueue_for_appservice(service, ephemeral=events)
|
||||
|
||||
@@ -351,7 +352,7 @@ class ApplicationServicesHandler:
|
||||
service, "presence", new_token
|
||||
)
|
||||
|
||||
elif stream_key == "to_device_key":
|
||||
elif stream_key == StreamKeyType.TO_DEVICE:
|
||||
# Retrieve a list of to-device message events, as well as the
|
||||
# maximum stream token of the messages we were able to retrieve.
|
||||
to_device_messages = await self._get_to_device_messages(
|
||||
@@ -366,7 +367,7 @@ class ApplicationServicesHandler:
|
||||
service, "to_device", new_token
|
||||
)
|
||||
|
||||
elif stream_key == "device_list_key":
|
||||
elif stream_key == StreamKeyType.DEVICE_LIST:
|
||||
device_list_summary = await self._get_device_list_summary(
|
||||
service, new_token
|
||||
)
|
||||
|
||||
@@ -43,6 +43,7 @@ from synapse.metrics.background_process_metrics import (
|
||||
)
|
||||
from synapse.types import (
|
||||
JsonDict,
|
||||
StreamKeyType,
|
||||
StreamToken,
|
||||
UserID,
|
||||
get_domain_from_id,
|
||||
@@ -502,7 +503,7 @@ class DeviceHandler(DeviceWorkerHandler):
|
||||
# specify the user ID too since the user should always get their own device list
|
||||
# updates, even if they aren't in any rooms.
|
||||
self.notifier.on_new_event(
|
||||
"device_list_key", position, users={user_id}, rooms=room_ids
|
||||
StreamKeyType.DEVICE_LIST, position, users={user_id}, rooms=room_ids
|
||||
)
|
||||
|
||||
# We may need to do some processing asynchronously for local user IDs.
|
||||
@@ -523,7 +524,9 @@ class DeviceHandler(DeviceWorkerHandler):
|
||||
from_user_id, user_ids
|
||||
)
|
||||
|
||||
self.notifier.on_new_event("device_list_key", position, users=[from_user_id])
|
||||
self.notifier.on_new_event(
|
||||
StreamKeyType.DEVICE_LIST, position, users=[from_user_id]
|
||||
)
|
||||
|
||||
async def user_left_room(self, user: UserID, room_id: str) -> None:
|
||||
user_id = user.to_string()
|
||||
|
||||
@@ -26,7 +26,7 @@ from synapse.logging.opentracing import (
|
||||
set_tag,
|
||||
)
|
||||
from synapse.replication.http.devices import ReplicationUserDevicesResyncRestServlet
|
||||
from synapse.types import JsonDict, Requester, UserID, get_domain_from_id
|
||||
from synapse.types import JsonDict, Requester, StreamKeyType, UserID, get_domain_from_id
|
||||
from synapse.util import json_encoder
|
||||
from synapse.util.stringutils import random_string
|
||||
|
||||
@@ -151,7 +151,7 @@ class DeviceMessageHandler:
|
||||
# Notify listeners that there are new to-device messages to process,
|
||||
# handing them the latest stream id.
|
||||
self.notifier.on_new_event(
|
||||
"to_device_key", last_stream_id, users=local_messages.keys()
|
||||
StreamKeyType.TO_DEVICE, last_stream_id, users=local_messages.keys()
|
||||
)
|
||||
|
||||
async def _check_for_unknown_devices(
|
||||
@@ -285,7 +285,7 @@ class DeviceMessageHandler:
|
||||
# Notify listeners that there are new to-device messages to process,
|
||||
# handing them the latest stream id.
|
||||
self.notifier.on_new_event(
|
||||
"to_device_key", last_stream_id, users=local_messages.keys()
|
||||
StreamKeyType.TO_DEVICE, last_stream_id, users=local_messages.keys()
|
||||
)
|
||||
|
||||
if self.federation_sender:
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
# limitations under the License.
|
||||
|
||||
import logging
|
||||
from typing import TYPE_CHECKING, Dict, Iterable, List, Optional, Tuple
|
||||
from typing import TYPE_CHECKING, Any, Dict, Iterable, List, Optional, Tuple
|
||||
|
||||
import attr
|
||||
from canonicaljson import encode_canonical_json
|
||||
@@ -1105,22 +1105,19 @@ class E2eKeysHandler:
|
||||
# can request over federation
|
||||
raise NotFoundError("No %s key found for %s" % (key_type, user_id))
|
||||
|
||||
(
|
||||
key,
|
||||
key_id,
|
||||
verify_key,
|
||||
) = await self._retrieve_cross_signing_keys_for_remote_user(user, key_type)
|
||||
|
||||
if key is None:
|
||||
cross_signing_keys = await self._retrieve_cross_signing_keys_for_remote_user(
|
||||
user, key_type
|
||||
)
|
||||
if cross_signing_keys is None:
|
||||
raise NotFoundError("No %s key found for %s" % (key_type, user_id))
|
||||
|
||||
return key, key_id, verify_key
|
||||
return cross_signing_keys
|
||||
|
||||
async def _retrieve_cross_signing_keys_for_remote_user(
|
||||
self,
|
||||
user: UserID,
|
||||
desired_key_type: str,
|
||||
) -> Tuple[Optional[dict], Optional[str], Optional[VerifyKey]]:
|
||||
) -> Optional[Tuple[Dict[str, Any], str, VerifyKey]]:
|
||||
"""Queries cross-signing keys for a remote user and saves them to the database
|
||||
|
||||
Only the key specified by `key_type` will be returned, while all retrieved keys
|
||||
@@ -1146,12 +1143,10 @@ class E2eKeysHandler:
|
||||
type(e),
|
||||
e,
|
||||
)
|
||||
return None, None, None
|
||||
return None
|
||||
|
||||
# Process each of the retrieved cross-signing keys
|
||||
desired_key = None
|
||||
desired_key_id = None
|
||||
desired_verify_key = None
|
||||
desired_key_data = None
|
||||
retrieved_device_ids = []
|
||||
for key_type in ["master", "self_signing"]:
|
||||
key_content = remote_result.get(key_type + "_key")
|
||||
@@ -1196,9 +1191,7 @@ class E2eKeysHandler:
|
||||
|
||||
# If this is the desired key type, save it and its ID/VerifyKey
|
||||
if key_type == desired_key_type:
|
||||
desired_key = key_content
|
||||
desired_verify_key = verify_key
|
||||
desired_key_id = key_id
|
||||
desired_key_data = key_content, key_id, verify_key
|
||||
|
||||
# At the same time, store this key in the db for subsequent queries
|
||||
await self.store.set_e2e_cross_signing_key(
|
||||
@@ -1212,7 +1205,7 @@ class E2eKeysHandler:
|
||||
user.to_string(), retrieved_device_ids
|
||||
)
|
||||
|
||||
return desired_key, desired_key_id, desired_verify_key
|
||||
return desired_key_data
|
||||
|
||||
|
||||
def _check_cross_signing_key(
|
||||
|
||||
@@ -659,7 +659,7 @@ class FederationHandler:
|
||||
# in the invitee's sync stream. It is stripped out for all other local users.
|
||||
event.unsigned["knock_room_state"] = stripped_room_state["knock_state_events"]
|
||||
|
||||
context = EventContext.for_outlier()
|
||||
context = EventContext.for_outlier(self.storage)
|
||||
stream_id = await self._federation_event_handler.persist_events_and_notify(
|
||||
event.room_id, [(event, context)]
|
||||
)
|
||||
@@ -848,7 +848,7 @@ class FederationHandler:
|
||||
)
|
||||
)
|
||||
|
||||
context = EventContext.for_outlier()
|
||||
context = EventContext.for_outlier(self.storage)
|
||||
await self._federation_event_handler.persist_events_and_notify(
|
||||
event.room_id, [(event, context)]
|
||||
)
|
||||
@@ -877,7 +877,7 @@ class FederationHandler:
|
||||
|
||||
await self.federation_client.send_leave(host_list, event)
|
||||
|
||||
context = EventContext.for_outlier()
|
||||
context = EventContext.for_outlier(self.storage)
|
||||
stream_id = await self._federation_event_handler.persist_events_and_notify(
|
||||
event.room_id, [(event, context)]
|
||||
)
|
||||
|
||||
@@ -103,7 +103,7 @@ class FederationEventHandler:
|
||||
self._event_creation_handler = hs.get_event_creation_handler()
|
||||
self._event_auth_handler = hs.get_event_auth_handler()
|
||||
self._message_handler = hs.get_message_handler()
|
||||
self._action_generator = hs.get_action_generator()
|
||||
self._bulk_push_rule_evaluator = hs.get_bulk_push_rule_evaluator()
|
||||
self._state_resolution_handler = hs.get_state_resolution_handler()
|
||||
# avoid a circular dependency by deferring execution here
|
||||
self._get_room_member_handler = hs.get_room_member_handler
|
||||
@@ -1423,7 +1423,7 @@ class FederationEventHandler:
|
||||
# we're not bothering about room state, so flag the event as an outlier.
|
||||
event.internal_metadata.outlier = True
|
||||
|
||||
context = EventContext.for_outlier()
|
||||
context = EventContext.for_outlier(self._storage)
|
||||
try:
|
||||
validate_event_for_room_version(room_version_obj, event)
|
||||
check_auth_rules_for_event(room_version_obj, event, auth)
|
||||
@@ -1874,10 +1874,10 @@ class FederationEventHandler:
|
||||
)
|
||||
|
||||
return EventContext.with_state(
|
||||
storage=self._storage,
|
||||
state_group=state_group,
|
||||
state_group_before_event=context.state_group_before_event,
|
||||
current_state_ids=current_state_ids,
|
||||
prev_state_ids=prev_state_ids,
|
||||
state_delta_due_to_event=state_updates,
|
||||
prev_group=prev_group,
|
||||
delta_ids=state_updates,
|
||||
partial_state=context.partial_state,
|
||||
@@ -1913,7 +1913,7 @@ class FederationEventHandler:
|
||||
min_depth,
|
||||
)
|
||||
else:
|
||||
await self._action_generator.handle_push_actions_for_event(
|
||||
await self._bulk_push_rule_evaluator.action_for_event_by_user(
|
||||
event, context
|
||||
)
|
||||
|
||||
|
||||
@@ -30,6 +30,7 @@ from synapse.types import (
|
||||
Requester,
|
||||
RoomStreamToken,
|
||||
StateMap,
|
||||
StreamKeyType,
|
||||
StreamToken,
|
||||
UserID,
|
||||
)
|
||||
@@ -143,7 +144,7 @@ class InitialSyncHandler:
|
||||
to_key=int(now_token.receipt_key),
|
||||
)
|
||||
if self.hs.config.experimental.msc2285_enabled:
|
||||
receipt = ReceiptEventSource.filter_out_private(receipt, user_id)
|
||||
receipt = ReceiptEventSource.filter_out_private_receipts(receipt, user_id)
|
||||
|
||||
tags_by_room = await self.store.get_tags_for_user(user_id)
|
||||
|
||||
@@ -220,8 +221,10 @@ class InitialSyncHandler:
|
||||
self.storage, user_id, messages
|
||||
)
|
||||
|
||||
start_token = now_token.copy_and_replace("room_key", token)
|
||||
end_token = now_token.copy_and_replace("room_key", room_end_token)
|
||||
start_token = now_token.copy_and_replace(StreamKeyType.ROOM, token)
|
||||
end_token = now_token.copy_and_replace(
|
||||
StreamKeyType.ROOM, room_end_token
|
||||
)
|
||||
time_now = self.clock.time_msec()
|
||||
|
||||
d["messages"] = {
|
||||
@@ -369,8 +372,8 @@ class InitialSyncHandler:
|
||||
self.storage, user_id, messages, is_peeking=is_peeking
|
||||
)
|
||||
|
||||
start_token = StreamToken.START.copy_and_replace("room_key", token)
|
||||
end_token = StreamToken.START.copy_and_replace("room_key", stream_token)
|
||||
start_token = StreamToken.START.copy_and_replace(StreamKeyType.ROOM, token)
|
||||
end_token = StreamToken.START.copy_and_replace(StreamKeyType.ROOM, stream_token)
|
||||
|
||||
time_now = self.clock.time_msec()
|
||||
|
||||
@@ -449,7 +452,9 @@ class InitialSyncHandler:
|
||||
if not receipts:
|
||||
return []
|
||||
if self.hs.config.experimental.msc2285_enabled:
|
||||
receipts = ReceiptEventSource.filter_out_private(receipts, user_id)
|
||||
receipts = ReceiptEventSource.filter_out_private_receipts(
|
||||
receipts, user_id
|
||||
)
|
||||
return receipts
|
||||
|
||||
presence, receipts, (messages, token) = await make_deferred_yieldable(
|
||||
@@ -472,7 +477,7 @@ class InitialSyncHandler:
|
||||
self.storage, user_id, messages, is_peeking=is_peeking
|
||||
)
|
||||
|
||||
start_token = now_token.copy_and_replace("room_key", token)
|
||||
start_token = now_token.copy_and_replace(StreamKeyType.ROOM, token)
|
||||
end_token = now_token
|
||||
|
||||
time_now = self.clock.time_msec()
|
||||
|
||||
@@ -44,7 +44,7 @@ from synapse.api.errors import (
|
||||
from synapse.api.room_versions import KNOWN_ROOM_VERSIONS, RoomVersions
|
||||
from synapse.api.urls import ConsentURIBuilder
|
||||
from synapse.event_auth import validate_event_for_room_version
|
||||
from synapse.events import EventBase
|
||||
from synapse.events import EventBase, relation_from_event
|
||||
from synapse.events.builder import EventBuilder
|
||||
from synapse.events.snapshot import EventContext
|
||||
from synapse.events.validator import EventValidator
|
||||
@@ -426,7 +426,7 @@ class EventCreationHandler:
|
||||
# This is to stop us from diverging history *too* much.
|
||||
self.limiter = Linearizer(max_count=5, name="room_event_creation_limit")
|
||||
|
||||
self.action_generator = hs.get_action_generator()
|
||||
self._bulk_push_rule_evaluator = hs.get_bulk_push_rule_evaluator()
|
||||
|
||||
self.spam_checker = hs.get_spam_checker()
|
||||
self.third_party_event_rules: "ThirdPartyEventRules" = (
|
||||
@@ -757,6 +757,10 @@ class EventCreationHandler:
|
||||
The previous version of the event is returned, if it is found in the
|
||||
event context. Otherwise, None is returned.
|
||||
"""
|
||||
if event.internal_metadata.is_outlier():
|
||||
# This can happen due to out of band memberships
|
||||
return None
|
||||
|
||||
prev_state_ids = await context.get_prev_state_ids()
|
||||
prev_event_id = prev_state_ids.get((event.type, event.state_key))
|
||||
if not prev_event_id:
|
||||
@@ -1001,7 +1005,7 @@ class EventCreationHandler:
|
||||
# after it is created
|
||||
if builder.internal_metadata.outlier:
|
||||
event.internal_metadata.outlier = True
|
||||
context = EventContext.for_outlier()
|
||||
context = EventContext.for_outlier(self.storage)
|
||||
elif (
|
||||
event.type == EventTypes.MSC2716_INSERTION
|
||||
and state_event_ids
|
||||
@@ -1056,20 +1060,11 @@ class EventCreationHandler:
|
||||
SynapseError if the event is invalid.
|
||||
"""
|
||||
|
||||
relation = event.content.get("m.relates_to")
|
||||
relation = relation_from_event(event)
|
||||
if not relation:
|
||||
return
|
||||
|
||||
relation_type = relation.get("rel_type")
|
||||
if not relation_type:
|
||||
return
|
||||
|
||||
# Ensure the parent is real.
|
||||
relates_to = relation.get("event_id")
|
||||
if not relates_to:
|
||||
return
|
||||
|
||||
parent_event = await self.store.get_event(relates_to, allow_none=True)
|
||||
parent_event = await self.store.get_event(relation.parent_id, allow_none=True)
|
||||
if parent_event:
|
||||
# And in the same room.
|
||||
if parent_event.room_id != event.room_id:
|
||||
@@ -1078,28 +1073,31 @@ class EventCreationHandler:
|
||||
else:
|
||||
# There must be some reason that the client knows the event exists,
|
||||
# see if there are existing relations. If so, assume everything is fine.
|
||||
if not await self.store.event_is_target_of_relation(relates_to):
|
||||
if not await self.store.event_is_target_of_relation(relation.parent_id):
|
||||
# Otherwise, the client can't know about the parent event!
|
||||
raise SynapseError(400, "Can't send relation to unknown event")
|
||||
|
||||
# If this event is an annotation then we check that that the sender
|
||||
# can't annotate the same way twice (e.g. stops users from liking an
|
||||
# event multiple times).
|
||||
if relation_type == RelationTypes.ANNOTATION:
|
||||
aggregation_key = relation["key"]
|
||||
if relation.rel_type == RelationTypes.ANNOTATION:
|
||||
aggregation_key = relation.aggregation_key
|
||||
|
||||
if aggregation_key is None:
|
||||
raise SynapseError(400, "Missing aggregation key")
|
||||
|
||||
if len(aggregation_key) > 500:
|
||||
raise SynapseError(400, "Aggregation key is too long")
|
||||
|
||||
already_exists = await self.store.has_user_annotated_event(
|
||||
relates_to, event.type, aggregation_key, event.sender
|
||||
relation.parent_id, event.type, aggregation_key, event.sender
|
||||
)
|
||||
if already_exists:
|
||||
raise SynapseError(400, "Can't send same reaction twice")
|
||||
|
||||
# Don't attempt to start a thread if the parent event is a relation.
|
||||
elif relation_type == RelationTypes.THREAD:
|
||||
if await self.store.event_includes_relation(relates_to):
|
||||
elif relation.rel_type == RelationTypes.THREAD:
|
||||
if await self.store.event_includes_relation(relation.parent_id):
|
||||
raise SynapseError(
|
||||
400, "Cannot start threads from an event with a relation"
|
||||
)
|
||||
@@ -1245,7 +1243,9 @@ class EventCreationHandler:
|
||||
# and `state_groups` because they have `prev_events` that aren't persisted yet
|
||||
# (historical messages persisted in reverse-chronological order).
|
||||
if not event.internal_metadata.is_historical():
|
||||
await self.action_generator.handle_push_actions_for_event(event, context)
|
||||
await self._bulk_push_rule_evaluator.action_for_event_by_user(
|
||||
event, context
|
||||
)
|
||||
|
||||
try:
|
||||
# If we're a worker we need to hit out to the master.
|
||||
|
||||
@@ -27,7 +27,7 @@ from synapse.handlers.room import ShutdownRoomResponse
|
||||
from synapse.metrics.background_process_metrics import run_as_background_process
|
||||
from synapse.storage.state import StateFilter
|
||||
from synapse.streams.config import PaginationConfig
|
||||
from synapse.types import JsonDict, Requester
|
||||
from synapse.types import JsonDict, Requester, StreamKeyType
|
||||
from synapse.util.async_helpers import ReadWriteLock
|
||||
from synapse.util.stringutils import random_string
|
||||
from synapse.visibility import filter_events_for_client
|
||||
@@ -448,7 +448,7 @@ class PaginationHandler:
|
||||
)
|
||||
# We expect `/messages` to use historic pagination tokens by default but
|
||||
# `/messages` should still works with live tokens when manually provided.
|
||||
assert from_token.room_key.topological
|
||||
assert from_token.room_key.topological is not None
|
||||
|
||||
if pagin_config.limit is None:
|
||||
# This shouldn't happen as we've set a default limit before this
|
||||
@@ -491,7 +491,7 @@ class PaginationHandler:
|
||||
|
||||
if leave_token.topological < curr_topo:
|
||||
from_token = from_token.copy_and_replace(
|
||||
"room_key", leave_token
|
||||
StreamKeyType.ROOM, leave_token
|
||||
)
|
||||
|
||||
await self.hs.get_federation_handler().maybe_backfill(
|
||||
@@ -513,7 +513,7 @@ class PaginationHandler:
|
||||
event_filter=event_filter,
|
||||
)
|
||||
|
||||
next_token = from_token.copy_and_replace("room_key", next_key)
|
||||
next_token = from_token.copy_and_replace(StreamKeyType.ROOM, next_key)
|
||||
|
||||
if events:
|
||||
if event_filter:
|
||||
|
||||
@@ -66,7 +66,7 @@ from synapse.replication.tcp.commands import ClearUserSyncsCommand
|
||||
from synapse.replication.tcp.streams import PresenceFederationStream, PresenceStream
|
||||
from synapse.storage.databases.main import DataStore
|
||||
from synapse.streams import EventSource
|
||||
from synapse.types import JsonDict, UserID, get_domain_from_id
|
||||
from synapse.types import JsonDict, StreamKeyType, UserID, get_domain_from_id
|
||||
from synapse.util.async_helpers import Linearizer
|
||||
from synapse.util.caches.descriptors import _CacheContext, cached
|
||||
from synapse.util.metrics import Measure
|
||||
@@ -522,7 +522,7 @@ class WorkerPresenceHandler(BasePresenceHandler):
|
||||
room_ids_to_states, users_to_states = parties
|
||||
|
||||
self.notifier.on_new_event(
|
||||
"presence_key",
|
||||
StreamKeyType.PRESENCE,
|
||||
stream_id,
|
||||
rooms=room_ids_to_states.keys(),
|
||||
users=users_to_states.keys(),
|
||||
@@ -1145,7 +1145,7 @@ class PresenceHandler(BasePresenceHandler):
|
||||
room_ids_to_states, users_to_states = parties
|
||||
|
||||
self.notifier.on_new_event(
|
||||
"presence_key",
|
||||
StreamKeyType.PRESENCE,
|
||||
stream_id,
|
||||
rooms=room_ids_to_states.keys(),
|
||||
users=[UserID.from_string(u) for u in users_to_states],
|
||||
|
||||
@@ -17,7 +17,13 @@ from typing import TYPE_CHECKING, Iterable, List, Optional, Tuple
|
||||
from synapse.api.constants import ReceiptTypes
|
||||
from synapse.appservice import ApplicationService
|
||||
from synapse.streams import EventSource
|
||||
from synapse.types import JsonDict, ReadReceipt, UserID, get_domain_from_id
|
||||
from synapse.types import (
|
||||
JsonDict,
|
||||
ReadReceipt,
|
||||
StreamKeyType,
|
||||
UserID,
|
||||
get_domain_from_id,
|
||||
)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from synapse.server import HomeServer
|
||||
@@ -129,7 +135,9 @@ class ReceiptsHandler:
|
||||
|
||||
affected_room_ids = list({r.room_id for r in receipts})
|
||||
|
||||
self.notifier.on_new_event("receipt_key", max_batch_id, rooms=affected_room_ids)
|
||||
self.notifier.on_new_event(
|
||||
StreamKeyType.RECEIPT, max_batch_id, rooms=affected_room_ids
|
||||
)
|
||||
# Note that the min here shouldn't be relied upon to be accurate.
|
||||
await self.hs.get_pusherpool().on_new_receipts(
|
||||
min_batch_id, max_batch_id, affected_room_ids
|
||||
@@ -165,43 +173,69 @@ class ReceiptEventSource(EventSource[int, JsonDict]):
|
||||
self.config = hs.config
|
||||
|
||||
@staticmethod
|
||||
def filter_out_private(events: List[JsonDict], user_id: str) -> List[JsonDict]:
|
||||
def filter_out_private_receipts(
|
||||
rooms: List[JsonDict], user_id: str
|
||||
) -> List[JsonDict]:
|
||||
"""
|
||||
This method takes in what is returned by
|
||||
get_linearized_receipts_for_rooms() and goes through read receipts
|
||||
filtering out m.read.private receipts if they were not sent by the
|
||||
current user.
|
||||
Filters a list of serialized receipts (as returned by /sync and /initialSync)
|
||||
and removes private read receipts of other users.
|
||||
|
||||
This operates on the return value of get_linearized_receipts_for_rooms(),
|
||||
which is wrapped in a cache. Care must be taken to ensure that the input
|
||||
values are not modified.
|
||||
|
||||
Args:
|
||||
rooms: A list of mappings, each mapping has a `content` field, which
|
||||
is a map of event ID -> receipt type -> user ID -> receipt information.
|
||||
|
||||
Returns:
|
||||
The same as rooms, but filtered.
|
||||
"""
|
||||
|
||||
visible_events = []
|
||||
result = []
|
||||
|
||||
# filter out private receipts the user shouldn't see
|
||||
for event in events:
|
||||
content = event.get("content", {})
|
||||
new_event = event.copy()
|
||||
new_event["content"] = {}
|
||||
# Iterate through each room's receipt content.
|
||||
for room in rooms:
|
||||
# The receipt content with other user's private read receipts removed.
|
||||
content = {}
|
||||
|
||||
for event_id, event_content in content.items():
|
||||
receipt_event = {}
|
||||
for receipt_type, receipt_content in event_content.items():
|
||||
if receipt_type == ReceiptTypes.READ_PRIVATE:
|
||||
user_rr = receipt_content.get(user_id, None)
|
||||
if user_rr:
|
||||
receipt_event[ReceiptTypes.READ_PRIVATE] = {
|
||||
user_id: user_rr.copy()
|
||||
}
|
||||
else:
|
||||
receipt_event[receipt_type] = receipt_content.copy()
|
||||
# Iterate over each event ID / receipts for that event.
|
||||
for event_id, orig_event_content in room.get("content", {}).items():
|
||||
event_content = orig_event_content
|
||||
# If there are private read receipts, additional logic is necessary.
|
||||
if ReceiptTypes.READ_PRIVATE in event_content:
|
||||
# Make a copy without private read receipts to avoid leaking
|
||||
# other user's private read receipts..
|
||||
event_content = {
|
||||
receipt_type: receipt_value
|
||||
for receipt_type, receipt_value in event_content.items()
|
||||
if receipt_type != ReceiptTypes.READ_PRIVATE
|
||||
}
|
||||
|
||||
# Only include the receipt event if it is non-empty.
|
||||
if receipt_event:
|
||||
new_event["content"][event_id] = receipt_event
|
||||
# Copy the current user's private read receipt from the
|
||||
# original content, if it exists.
|
||||
user_private_read_receipt = orig_event_content[
|
||||
ReceiptTypes.READ_PRIVATE
|
||||
].get(user_id, None)
|
||||
if user_private_read_receipt:
|
||||
event_content[ReceiptTypes.READ_PRIVATE] = {
|
||||
user_id: user_private_read_receipt
|
||||
}
|
||||
|
||||
# Append new_event to visible_events unless empty
|
||||
if len(new_event["content"].keys()) > 0:
|
||||
visible_events.append(new_event)
|
||||
# Include the event if there is at least one non-private read
|
||||
# receipt or the current user has a private read receipt.
|
||||
if event_content:
|
||||
content[event_id] = event_content
|
||||
|
||||
return visible_events
|
||||
# Include the event if there is at least one non-private read receipt
|
||||
# or the current user has a private read receipt.
|
||||
if content:
|
||||
# Build a new event to avoid mutating the cache.
|
||||
new_room = {k: v for k, v in room.items() if k != "content"}
|
||||
new_room["content"] = content
|
||||
result.append(new_room)
|
||||
|
||||
return result
|
||||
|
||||
async def get_new_events(
|
||||
self,
|
||||
@@ -223,7 +257,9 @@ class ReceiptEventSource(EventSource[int, JsonDict]):
|
||||
)
|
||||
|
||||
if self.config.experimental.msc2285_enabled:
|
||||
events = ReceiptEventSource.filter_out_private(events, user.to_string())
|
||||
events = ReceiptEventSource.filter_out_private_receipts(
|
||||
events, user.to_string()
|
||||
)
|
||||
|
||||
return events, to_key
|
||||
|
||||
|
||||
@@ -11,7 +11,6 @@
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
import collections.abc
|
||||
import logging
|
||||
from typing import (
|
||||
TYPE_CHECKING,
|
||||
@@ -28,7 +27,7 @@ import attr
|
||||
|
||||
from synapse.api.constants import RelationTypes
|
||||
from synapse.api.errors import SynapseError
|
||||
from synapse.events import EventBase
|
||||
from synapse.events import EventBase, relation_from_event
|
||||
from synapse.storage.databases.main.relations import _RelatedEvent
|
||||
from synapse.types import JsonDict, Requester, StreamToken, UserID
|
||||
from synapse.visibility import filter_events_for_client
|
||||
@@ -373,20 +372,21 @@ class RelationsHandler:
|
||||
if event.is_state():
|
||||
continue
|
||||
|
||||
relates_to = event.content.get("m.relates_to")
|
||||
relation_type = None
|
||||
if isinstance(relates_to, collections.abc.Mapping):
|
||||
relation_type = relates_to.get("rel_type")
|
||||
relates_to = relation_from_event(event)
|
||||
if relates_to:
|
||||
# An event which is a replacement (ie edit) or annotation (ie,
|
||||
# reaction) may not have any other event related to it.
|
||||
if relation_type in (RelationTypes.ANNOTATION, RelationTypes.REPLACE):
|
||||
if relates_to.rel_type in (
|
||||
RelationTypes.ANNOTATION,
|
||||
RelationTypes.REPLACE,
|
||||
):
|
||||
continue
|
||||
|
||||
# Track the event's relation information for later.
|
||||
relations_by_id[event.event_id] = relates_to.rel_type
|
||||
|
||||
# The event should get bundled aggregations.
|
||||
events_by_id[event.event_id] = event
|
||||
# Track the event's relation information for later.
|
||||
if isinstance(relation_type, str):
|
||||
relations_by_id[event.event_id] = relation_type
|
||||
|
||||
# event ID -> bundled aggregation in non-serialized form.
|
||||
results: Dict[str, BundledAggregations] = {}
|
||||
|
||||
@@ -33,6 +33,7 @@ from typing import (
|
||||
import attr
|
||||
from typing_extensions import TypedDict
|
||||
|
||||
import synapse.events.snapshot
|
||||
from synapse.api.constants import (
|
||||
EventContentFields,
|
||||
EventTypes,
|
||||
@@ -72,12 +73,12 @@ from synapse.types import (
|
||||
RoomID,
|
||||
RoomStreamToken,
|
||||
StateMap,
|
||||
StreamKeyType,
|
||||
StreamToken,
|
||||
UserID,
|
||||
create_requester,
|
||||
)
|
||||
from synapse.util import stringutils
|
||||
from synapse.util.async_helpers import Linearizer
|
||||
from synapse.util.caches.response_cache import ResponseCache
|
||||
from synapse.util.stringutils import parse_and_validate_server_name
|
||||
from synapse.visibility import filter_events_for_client
|
||||
@@ -149,10 +150,11 @@ class RoomCreationHandler:
|
||||
)
|
||||
preset_config["encrypted"] = encrypted
|
||||
|
||||
self._replication = hs.get_replication_data_handler()
|
||||
self._default_power_level_content_override = (
|
||||
self.config.room.default_power_level_content_override
|
||||
)
|
||||
|
||||
# linearizer to stop two upgrades happening at once
|
||||
self._upgrade_linearizer = Linearizer("room_upgrade_linearizer")
|
||||
self._replication = hs.get_replication_data_handler()
|
||||
|
||||
# If a user tries to update the same room multiple times in quick
|
||||
# succession, only process the first attempt and return its result to
|
||||
@@ -196,50 +198,17 @@ class RoomCreationHandler:
|
||||
400, "An upgrade for this room is currently in progress"
|
||||
)
|
||||
|
||||
# Upgrade the room
|
||||
#
|
||||
# If this user has sent multiple upgrade requests for the same room
|
||||
# and one of them is not complete yet, cache the response and
|
||||
# return it to all subsequent requests
|
||||
ret = await self._upgrade_response_cache.wrap(
|
||||
(old_room_id, user_id),
|
||||
self._upgrade_room,
|
||||
requester,
|
||||
old_room_id,
|
||||
new_version, # args for _upgrade_room
|
||||
)
|
||||
|
||||
return ret
|
||||
|
||||
async def _upgrade_room(
|
||||
self, requester: Requester, old_room_id: str, new_version: RoomVersion
|
||||
) -> str:
|
||||
"""
|
||||
Args:
|
||||
requester: the user requesting the upgrade
|
||||
old_room_id: the id of the room to be replaced
|
||||
new_versions: the version to upgrade the room to
|
||||
|
||||
Raises:
|
||||
ShadowBanError if the requester is shadow-banned.
|
||||
"""
|
||||
user_id = requester.user.to_string()
|
||||
assert self.hs.is_mine_id(user_id), "User must be our own: %s" % (user_id,)
|
||||
|
||||
# start by allocating a new room id
|
||||
r = await self.store.get_room(old_room_id)
|
||||
if r is None:
|
||||
# Check whether the room exists and 404 if it doesn't.
|
||||
# We could go straight for the auth check, but that will raise a 403 instead.
|
||||
old_room = await self.store.get_room(old_room_id)
|
||||
if old_room is None:
|
||||
raise NotFoundError("Unknown room id %s" % (old_room_id,))
|
||||
new_room_id = await self._generate_room_id(
|
||||
creator_id=user_id,
|
||||
is_public=r["is_public"],
|
||||
room_version=new_version,
|
||||
)
|
||||
|
||||
logger.info("Creating new room %s to replace %s", new_room_id, old_room_id)
|
||||
new_room_id = self._generate_room_id()
|
||||
|
||||
# we create and auth the tombstone event before properly creating the new
|
||||
# room, to check our user has perms in the old room.
|
||||
# Check whether the user has the power level to carry out the upgrade.
|
||||
# `check_auth_rules_from_context` will check that they are in the room and have
|
||||
# the required power level to send the tombstone event.
|
||||
(
|
||||
tombstone_event,
|
||||
tombstone_context,
|
||||
@@ -262,6 +231,63 @@ class RoomCreationHandler:
|
||||
old_room_version, tombstone_event, tombstone_context
|
||||
)
|
||||
|
||||
# Upgrade the room
|
||||
#
|
||||
# If this user has sent multiple upgrade requests for the same room
|
||||
# and one of them is not complete yet, cache the response and
|
||||
# return it to all subsequent requests
|
||||
ret = await self._upgrade_response_cache.wrap(
|
||||
(old_room_id, user_id),
|
||||
self._upgrade_room,
|
||||
requester,
|
||||
old_room_id,
|
||||
old_room, # args for _upgrade_room
|
||||
new_room_id,
|
||||
new_version,
|
||||
tombstone_event,
|
||||
tombstone_context,
|
||||
)
|
||||
|
||||
return ret
|
||||
|
||||
async def _upgrade_room(
|
||||
self,
|
||||
requester: Requester,
|
||||
old_room_id: str,
|
||||
old_room: Dict[str, Any],
|
||||
new_room_id: str,
|
||||
new_version: RoomVersion,
|
||||
tombstone_event: EventBase,
|
||||
tombstone_context: synapse.events.snapshot.EventContext,
|
||||
) -> str:
|
||||
"""
|
||||
Args:
|
||||
requester: the user requesting the upgrade
|
||||
old_room_id: the id of the room to be replaced
|
||||
old_room: a dict containing room information for the room to be replaced,
|
||||
as returned by `RoomWorkerStore.get_room`.
|
||||
new_room_id: the id of the replacement room
|
||||
new_version: the version to upgrade the room to
|
||||
tombstone_event: the tombstone event to send to the old room
|
||||
tombstone_context: the context for the tombstone event
|
||||
|
||||
Raises:
|
||||
ShadowBanError if the requester is shadow-banned.
|
||||
"""
|
||||
user_id = requester.user.to_string()
|
||||
assert self.hs.is_mine_id(user_id), "User must be our own: %s" % (user_id,)
|
||||
|
||||
logger.info("Creating new room %s to replace %s", new_room_id, old_room_id)
|
||||
|
||||
# create the new room. may raise a `StoreError` in the exceedingly unlikely
|
||||
# event of a room ID collision.
|
||||
await self.store.store_room(
|
||||
room_id=new_room_id,
|
||||
room_creator_user_id=user_id,
|
||||
is_public=old_room["is_public"],
|
||||
room_version=new_version,
|
||||
)
|
||||
|
||||
await self.clone_existing_room(
|
||||
requester,
|
||||
old_room_id=old_room_id,
|
||||
@@ -778,7 +804,7 @@ class RoomCreationHandler:
|
||||
visibility = config.get("visibility", "private")
|
||||
is_public = visibility == "public"
|
||||
|
||||
room_id = await self._generate_room_id(
|
||||
room_id = await self._generate_and_create_room_id(
|
||||
creator_id=user_id,
|
||||
is_public=is_public,
|
||||
room_version=room_version,
|
||||
@@ -1042,9 +1068,19 @@ class RoomCreationHandler:
|
||||
for invitee in invite_list:
|
||||
power_level_content["users"][invitee] = 100
|
||||
|
||||
# Power levels overrides are defined per chat preset
|
||||
# If the user supplied a preset name e.g. "private_chat",
|
||||
# we apply that preset
|
||||
power_level_content.update(config["power_level_content_override"])
|
||||
|
||||
# If the server config contains default_power_level_content_override,
|
||||
# and that contains information for this room preset, apply it.
|
||||
if self._default_power_level_content_override:
|
||||
override = self._default_power_level_content_override.get(preset_config)
|
||||
if override is not None:
|
||||
power_level_content.update(override)
|
||||
|
||||
# Finally, if the user supplied specific permissions for this room,
|
||||
# apply those.
|
||||
if power_level_content_override:
|
||||
power_level_content.update(power_level_content_override)
|
||||
|
||||
@@ -1090,7 +1126,26 @@ class RoomCreationHandler:
|
||||
|
||||
return last_sent_stream_id
|
||||
|
||||
async def _generate_room_id(
|
||||
def _generate_room_id(self) -> str:
|
||||
"""Generates a random room ID.
|
||||
|
||||
Room IDs look like "!opaque_id:domain" and are case-sensitive as per the spec
|
||||
at https://spec.matrix.org/v1.2/appendices/#room-ids-and-event-ids.
|
||||
|
||||
Does not check for collisions with existing rooms or prevent future calls from
|
||||
returning the same room ID. To ensure the uniqueness of a new room ID, use
|
||||
`_generate_and_create_room_id` instead.
|
||||
|
||||
Synapse's room IDs are 18 [a-zA-Z] characters long, which comes out to around
|
||||
102 bits.
|
||||
|
||||
Returns:
|
||||
A random room ID of the form "!opaque_id:domain".
|
||||
"""
|
||||
random_string = stringutils.random_string(18)
|
||||
return RoomID(random_string, self.hs.hostname).to_string()
|
||||
|
||||
async def _generate_and_create_room_id(
|
||||
self,
|
||||
creator_id: str,
|
||||
is_public: bool,
|
||||
@@ -1101,8 +1156,7 @@ class RoomCreationHandler:
|
||||
attempts = 0
|
||||
while attempts < 5:
|
||||
try:
|
||||
random_string = stringutils.random_string(18)
|
||||
gen_room_id = RoomID(random_string, self.hs.hostname).to_string()
|
||||
gen_room_id = self._generate_room_id()
|
||||
await self.store.store_room(
|
||||
room_id=gen_room_id,
|
||||
room_creator_user_id=creator_id,
|
||||
@@ -1239,10 +1293,10 @@ class RoomContextHandler:
|
||||
events_after=events_after,
|
||||
state=await filter_evts(state_events),
|
||||
aggregations=aggregations,
|
||||
start=await token.copy_and_replace("room_key", results.start).to_string(
|
||||
self.store
|
||||
),
|
||||
end=await token.copy_and_replace("room_key", results.end).to_string(
|
||||
start=await token.copy_and_replace(
|
||||
StreamKeyType.ROOM, results.start
|
||||
).to_string(self.store),
|
||||
end=await token.copy_and_replace(StreamKeyType.ROOM, results.end).to_string(
|
||||
self.store
|
||||
),
|
||||
)
|
||||
|
||||
@@ -24,7 +24,7 @@ from synapse.api.errors import NotFoundError, SynapseError
|
||||
from synapse.api.filtering import Filter
|
||||
from synapse.events import EventBase
|
||||
from synapse.storage.state import StateFilter
|
||||
from synapse.types import JsonDict, UserID
|
||||
from synapse.types import JsonDict, StreamKeyType, UserID
|
||||
from synapse.visibility import filter_events_for_client
|
||||
|
||||
if TYPE_CHECKING:
|
||||
@@ -655,11 +655,11 @@ class SearchHandler:
|
||||
"events_before": events_before,
|
||||
"events_after": events_after,
|
||||
"start": await now_token.copy_and_replace(
|
||||
"room_key", res.start
|
||||
StreamKeyType.ROOM, res.start
|
||||
).to_string(self.store),
|
||||
"end": await now_token.copy_and_replace(
|
||||
StreamKeyType.ROOM, res.end
|
||||
).to_string(self.store),
|
||||
"end": await now_token.copy_and_replace("room_key", res.end).to_string(
|
||||
self.store
|
||||
),
|
||||
}
|
||||
|
||||
if include_profile:
|
||||
|
||||
@@ -37,6 +37,7 @@ from synapse.types import (
|
||||
Requester,
|
||||
RoomStreamToken,
|
||||
StateMap,
|
||||
StreamKeyType,
|
||||
StreamToken,
|
||||
UserID,
|
||||
)
|
||||
@@ -449,7 +450,7 @@ class SyncHandler:
|
||||
room_ids=room_ids,
|
||||
is_guest=sync_config.is_guest,
|
||||
)
|
||||
now_token = now_token.copy_and_replace("typing_key", typing_key)
|
||||
now_token = now_token.copy_and_replace(StreamKeyType.TYPING, typing_key)
|
||||
|
||||
ephemeral_by_room: JsonDict = {}
|
||||
|
||||
@@ -471,7 +472,7 @@ class SyncHandler:
|
||||
room_ids=room_ids,
|
||||
is_guest=sync_config.is_guest,
|
||||
)
|
||||
now_token = now_token.copy_and_replace("receipt_key", receipt_key)
|
||||
now_token = now_token.copy_and_replace(StreamKeyType.RECEIPT, receipt_key)
|
||||
|
||||
for event in receipts:
|
||||
room_id = event["room_id"]
|
||||
@@ -537,7 +538,9 @@ class SyncHandler:
|
||||
prev_batch_token = now_token
|
||||
if recents:
|
||||
room_key = recents[0].internal_metadata.before
|
||||
prev_batch_token = now_token.copy_and_replace("room_key", room_key)
|
||||
prev_batch_token = now_token.copy_and_replace(
|
||||
StreamKeyType.ROOM, room_key
|
||||
)
|
||||
|
||||
return TimelineBatch(
|
||||
events=recents, prev_batch=prev_batch_token, limited=False
|
||||
@@ -611,7 +614,7 @@ class SyncHandler:
|
||||
recents = recents[-timeline_limit:]
|
||||
room_key = recents[0].internal_metadata.before
|
||||
|
||||
prev_batch_token = now_token.copy_and_replace("room_key", room_key)
|
||||
prev_batch_token = now_token.copy_and_replace(StreamKeyType.ROOM, room_key)
|
||||
|
||||
# Don't bother to bundle aggregations if the timeline is unlimited,
|
||||
# as clients will have all the necessary information.
|
||||
@@ -1398,7 +1401,7 @@ class SyncHandler:
|
||||
now_token.to_device_key,
|
||||
)
|
||||
sync_result_builder.now_token = now_token.copy_and_replace(
|
||||
"to_device_key", stream_id
|
||||
StreamKeyType.TO_DEVICE, stream_id
|
||||
)
|
||||
sync_result_builder.to_device = messages
|
||||
else:
|
||||
@@ -1503,7 +1506,7 @@ class SyncHandler:
|
||||
)
|
||||
assert presence_key
|
||||
sync_result_builder.now_token = now_token.copy_and_replace(
|
||||
"presence_key", presence_key
|
||||
StreamKeyType.PRESENCE, presence_key
|
||||
)
|
||||
|
||||
extra_users_ids = set(newly_joined_or_invited_users)
|
||||
@@ -1826,7 +1829,7 @@ class SyncHandler:
|
||||
# stream token as it'll only be used in the context of this
|
||||
# room. (c.f. the docstring of `to_room_stream_token`).
|
||||
leave_token = since_token.copy_and_replace(
|
||||
"room_key", leave_position.to_room_stream_token()
|
||||
StreamKeyType.ROOM, leave_position.to_room_stream_token()
|
||||
)
|
||||
|
||||
# If this is an out of band message, like a remote invite
|
||||
@@ -1875,7 +1878,9 @@ class SyncHandler:
|
||||
if room_entry:
|
||||
events, start_key = room_entry
|
||||
|
||||
prev_batch_token = now_token.copy_and_replace("room_key", start_key)
|
||||
prev_batch_token = now_token.copy_and_replace(
|
||||
StreamKeyType.ROOM, start_key
|
||||
)
|
||||
|
||||
entry = RoomSyncResultBuilder(
|
||||
room_id=room_id,
|
||||
@@ -1972,7 +1977,7 @@ class SyncHandler:
|
||||
continue
|
||||
|
||||
leave_token = now_token.copy_and_replace(
|
||||
"room_key", RoomStreamToken(None, event.stream_ordering)
|
||||
StreamKeyType.ROOM, RoomStreamToken(None, event.stream_ordering)
|
||||
)
|
||||
room_entries.append(
|
||||
RoomSyncResultBuilder(
|
||||
|
||||
@@ -25,7 +25,7 @@ from synapse.metrics.background_process_metrics import (
|
||||
)
|
||||
from synapse.replication.tcp.streams import TypingStream
|
||||
from synapse.streams import EventSource
|
||||
from synapse.types import JsonDict, Requester, UserID, get_domain_from_id
|
||||
from synapse.types import JsonDict, Requester, StreamKeyType, UserID, get_domain_from_id
|
||||
from synapse.util.caches.stream_change_cache import StreamChangeCache
|
||||
from synapse.util.metrics import Measure
|
||||
from synapse.util.wheel_timer import WheelTimer
|
||||
@@ -382,7 +382,7 @@ class TypingWriterHandler(FollowerTypingHandler):
|
||||
)
|
||||
|
||||
self.notifier.on_new_event(
|
||||
"typing_key", self._latest_room_serial, rooms=[member.room_id]
|
||||
StreamKeyType.TYPING, self._latest_room_serial, rooms=[member.room_id]
|
||||
)
|
||||
|
||||
async def get_all_typing_updates(
|
||||
|
||||
@@ -43,8 +43,10 @@ from twisted.internet import defer, error as twisted_error, protocol, ssl
|
||||
from twisted.internet.address import IPv4Address, IPv6Address
|
||||
from twisted.internet.interfaces import (
|
||||
IAddress,
|
||||
IDelayedCall,
|
||||
IHostResolution,
|
||||
IReactorPluggableNameResolver,
|
||||
IReactorTime,
|
||||
IResolutionReceiver,
|
||||
ITCPTransport,
|
||||
)
|
||||
@@ -121,13 +123,15 @@ def check_against_blacklist(
|
||||
_EPSILON = 0.00000001
|
||||
|
||||
|
||||
def _make_scheduler(reactor):
|
||||
def _make_scheduler(
|
||||
reactor: IReactorTime,
|
||||
) -> Callable[[Callable[[], object]], IDelayedCall]:
|
||||
"""Makes a schedular suitable for a Cooperator using the given reactor.
|
||||
|
||||
(This is effectively just a copy from `twisted.internet.task`)
|
||||
"""
|
||||
|
||||
def _scheduler(x):
|
||||
def _scheduler(x: Callable[[], object]) -> IDelayedCall:
|
||||
return reactor.callLater(_EPSILON, x)
|
||||
|
||||
return _scheduler
|
||||
@@ -348,7 +352,7 @@ class SimpleHttpClient:
|
||||
# XXX: The justification for using the cache factor here is that larger instances
|
||||
# will need both more cache and more connections.
|
||||
# Still, this should probably be a separate dial
|
||||
pool.maxPersistentPerHost = max((100 * hs.config.caches.global_factor, 5))
|
||||
pool.maxPersistentPerHost = max(int(100 * hs.config.caches.global_factor), 5)
|
||||
pool.cachedConnectionTimeout = 2 * 60
|
||||
|
||||
self.agent: IAgent = ProxyAgent(
|
||||
@@ -775,7 +779,7 @@ class SimpleHttpClient:
|
||||
)
|
||||
|
||||
|
||||
def _timeout_to_request_timed_out_error(f: Failure):
|
||||
def _timeout_to_request_timed_out_error(f: Failure) -> Failure:
|
||||
if f.check(twisted_error.TimeoutError, twisted_error.ConnectingCancelledError):
|
||||
# The TCP connection has its own timeout (set by the 'connectTimeout' param
|
||||
# on the Agent), which raises twisted_error.TimeoutError exception.
|
||||
@@ -809,7 +813,7 @@ class _DiscardBodyWithMaxSizeProtocol(protocol.Protocol):
|
||||
def __init__(self, deferred: defer.Deferred):
|
||||
self.deferred = deferred
|
||||
|
||||
def _maybe_fail(self):
|
||||
def _maybe_fail(self) -> None:
|
||||
"""
|
||||
Report a max size exceed error and disconnect the first time this is called.
|
||||
"""
|
||||
@@ -933,12 +937,12 @@ class InsecureInterceptableContextFactory(ssl.ContextFactory):
|
||||
Do not use this since it allows an attacker to intercept your communications.
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
def __init__(self) -> None:
|
||||
self._context = SSL.Context(SSL.SSLv23_METHOD)
|
||||
self._context.set_verify(VERIFY_NONE, lambda *_: False)
|
||||
|
||||
def getContext(self, hostname=None, port=None):
|
||||
return self._context
|
||||
|
||||
def creatorForNetloc(self, hostname, port):
|
||||
def creatorForNetloc(self, hostname: bytes, port: int):
|
||||
return self
|
||||
|
||||
@@ -14,15 +14,22 @@
|
||||
|
||||
import base64
|
||||
import logging
|
||||
from typing import Optional
|
||||
from typing import Optional, Union
|
||||
|
||||
import attr
|
||||
from zope.interface import implementer
|
||||
|
||||
from twisted.internet import defer, protocol
|
||||
from twisted.internet.error import ConnectError
|
||||
from twisted.internet.interfaces import IReactorCore, IStreamClientEndpoint
|
||||
from twisted.internet.interfaces import (
|
||||
IAddress,
|
||||
IConnector,
|
||||
IProtocol,
|
||||
IReactorCore,
|
||||
IStreamClientEndpoint,
|
||||
)
|
||||
from twisted.internet.protocol import ClientFactory, Protocol, connectionDone
|
||||
from twisted.python.failure import Failure
|
||||
from twisted.web import http
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
@@ -81,14 +88,14 @@ class HTTPConnectProxyEndpoint:
|
||||
self._port = port
|
||||
self._proxy_creds = proxy_creds
|
||||
|
||||
def __repr__(self):
|
||||
def __repr__(self) -> str:
|
||||
return "<HTTPConnectProxyEndpoint %s>" % (self._proxy_endpoint,)
|
||||
|
||||
# Mypy encounters a false positive here: it complains that ClientFactory
|
||||
# is incompatible with IProtocolFactory. But ClientFactory inherits from
|
||||
# Factory, which implements IProtocolFactory. So I think this is a bug
|
||||
# in mypy-zope.
|
||||
def connect(self, protocolFactory: ClientFactory): # type: ignore[override]
|
||||
def connect(self, protocolFactory: ClientFactory) -> "defer.Deferred[IProtocol]": # type: ignore[override]
|
||||
f = HTTPProxiedClientFactory(
|
||||
self._host, self._port, protocolFactory, self._proxy_creds
|
||||
)
|
||||
@@ -125,10 +132,10 @@ class HTTPProxiedClientFactory(protocol.ClientFactory):
|
||||
self.proxy_creds = proxy_creds
|
||||
self.on_connection: "defer.Deferred[None]" = defer.Deferred()
|
||||
|
||||
def startedConnecting(self, connector):
|
||||
def startedConnecting(self, connector: IConnector) -> None:
|
||||
return self.wrapped_factory.startedConnecting(connector)
|
||||
|
||||
def buildProtocol(self, addr):
|
||||
def buildProtocol(self, addr: IAddress) -> "HTTPConnectProtocol":
|
||||
wrapped_protocol = self.wrapped_factory.buildProtocol(addr)
|
||||
if wrapped_protocol is None:
|
||||
raise TypeError("buildProtocol produced None instead of a Protocol")
|
||||
@@ -141,13 +148,13 @@ class HTTPProxiedClientFactory(protocol.ClientFactory):
|
||||
self.proxy_creds,
|
||||
)
|
||||
|
||||
def clientConnectionFailed(self, connector, reason):
|
||||
def clientConnectionFailed(self, connector: IConnector, reason: Failure) -> None:
|
||||
logger.debug("Connection to proxy failed: %s", reason)
|
||||
if not self.on_connection.called:
|
||||
self.on_connection.errback(reason)
|
||||
return self.wrapped_factory.clientConnectionFailed(connector, reason)
|
||||
|
||||
def clientConnectionLost(self, connector, reason):
|
||||
def clientConnectionLost(self, connector: IConnector, reason: Failure) -> None:
|
||||
logger.debug("Connection to proxy lost: %s", reason)
|
||||
if not self.on_connection.called:
|
||||
self.on_connection.errback(reason)
|
||||
@@ -191,10 +198,10 @@ class HTTPConnectProtocol(protocol.Protocol):
|
||||
)
|
||||
self.http_setup_client.on_connected.addCallback(self.proxyConnected)
|
||||
|
||||
def connectionMade(self):
|
||||
def connectionMade(self) -> None:
|
||||
self.http_setup_client.makeConnection(self.transport)
|
||||
|
||||
def connectionLost(self, reason=connectionDone):
|
||||
def connectionLost(self, reason: Failure = connectionDone) -> None:
|
||||
if self.wrapped_protocol.connected:
|
||||
self.wrapped_protocol.connectionLost(reason)
|
||||
|
||||
@@ -203,7 +210,7 @@ class HTTPConnectProtocol(protocol.Protocol):
|
||||
if not self.connected_deferred.called:
|
||||
self.connected_deferred.errback(reason)
|
||||
|
||||
def proxyConnected(self, _):
|
||||
def proxyConnected(self, _: Union[None, "defer.Deferred[None]"]) -> None:
|
||||
self.wrapped_protocol.makeConnection(self.transport)
|
||||
|
||||
self.connected_deferred.callback(self.wrapped_protocol)
|
||||
@@ -213,7 +220,7 @@ class HTTPConnectProtocol(protocol.Protocol):
|
||||
if buf:
|
||||
self.wrapped_protocol.dataReceived(buf)
|
||||
|
||||
def dataReceived(self, data: bytes):
|
||||
def dataReceived(self, data: bytes) -> None:
|
||||
# if we've set up the HTTP protocol, we can send the data there
|
||||
if self.wrapped_protocol.connected:
|
||||
return self.wrapped_protocol.dataReceived(data)
|
||||
@@ -243,7 +250,7 @@ class HTTPConnectSetupClient(http.HTTPClient):
|
||||
self.proxy_creds = proxy_creds
|
||||
self.on_connected: "defer.Deferred[None]" = defer.Deferred()
|
||||
|
||||
def connectionMade(self):
|
||||
def connectionMade(self) -> None:
|
||||
logger.debug("Connected to proxy, sending CONNECT")
|
||||
self.sendCommand(b"CONNECT", b"%s:%d" % (self.host, self.port))
|
||||
|
||||
@@ -257,14 +264,14 @@ class HTTPConnectSetupClient(http.HTTPClient):
|
||||
|
||||
self.endHeaders()
|
||||
|
||||
def handleStatus(self, version: bytes, status: bytes, message: bytes):
|
||||
def handleStatus(self, version: bytes, status: bytes, message: bytes) -> None:
|
||||
logger.debug("Got Status: %s %s %s", status, message, version)
|
||||
if status != b"200":
|
||||
raise ProxyConnectError(f"Unexpected status on CONNECT: {status!s}")
|
||||
|
||||
def handleEndHeaders(self):
|
||||
def handleEndHeaders(self) -> None:
|
||||
logger.debug("End Headers")
|
||||
self.on_connected.callback(None)
|
||||
|
||||
def handleResponse(self, body):
|
||||
def handleResponse(self, body: bytes) -> None:
|
||||
pass
|
||||
|
||||
@@ -239,7 +239,7 @@ class MatrixHostnameEndpointFactory:
|
||||
|
||||
self._srv_resolver = srv_resolver
|
||||
|
||||
def endpointForURI(self, parsed_uri: URI):
|
||||
def endpointForURI(self, parsed_uri: URI) -> "MatrixHostnameEndpoint":
|
||||
return MatrixHostnameEndpoint(
|
||||
self._reactor,
|
||||
self._proxy_reactor,
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
import logging
|
||||
import random
|
||||
import time
|
||||
from typing import Callable, Dict, List
|
||||
from typing import Any, Callable, Dict, List
|
||||
|
||||
import attr
|
||||
|
||||
@@ -109,7 +109,7 @@ class SrvResolver:
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
dns_client=client,
|
||||
dns_client: Any = client,
|
||||
cache: Dict[bytes, List[Server]] = SERVER_CACHE,
|
||||
get_time: Callable[[], float] = time.time,
|
||||
):
|
||||
|
||||
@@ -74,9 +74,9 @@ _well_known_cache: TTLCache[bytes, Optional[bytes]] = TTLCache("well-known")
|
||||
_had_valid_well_known_cache: TTLCache[bytes, bool] = TTLCache("had-valid-well-known")
|
||||
|
||||
|
||||
@attr.s(slots=True, frozen=True)
|
||||
@attr.s(slots=True, frozen=True, auto_attribs=True)
|
||||
class WellKnownLookupResult:
|
||||
delegated_server = attr.ib()
|
||||
delegated_server: Optional[bytes]
|
||||
|
||||
|
||||
class WellKnownResolver:
|
||||
@@ -336,4 +336,4 @@ def _parse_cache_control(headers: Headers) -> Dict[bytes, Optional[bytes]]:
|
||||
class _FetchWellKnownFailure(Exception):
|
||||
# True if we didn't get a non-5xx HTTP response, i.e. this may or may not be
|
||||
# a temporary failure.
|
||||
temporary = attr.ib()
|
||||
temporary: bool = attr.ib()
|
||||
|
||||
@@ -23,6 +23,8 @@ from http import HTTPStatus
|
||||
from io import BytesIO, StringIO
|
||||
from typing import (
|
||||
TYPE_CHECKING,
|
||||
Any,
|
||||
BinaryIO,
|
||||
Callable,
|
||||
Dict,
|
||||
Generic,
|
||||
@@ -44,7 +46,7 @@ from typing_extensions import Literal
|
||||
from twisted.internet import defer
|
||||
from twisted.internet.error import DNSLookupError
|
||||
from twisted.internet.interfaces import IReactorTime
|
||||
from twisted.internet.task import _EPSILON, Cooperator
|
||||
from twisted.internet.task import Cooperator
|
||||
from twisted.web.client import ResponseFailed
|
||||
from twisted.web.http_headers import Headers
|
||||
from twisted.web.iweb import IBodyProducer, IResponse
|
||||
@@ -58,11 +60,13 @@ from synapse.api.errors import (
|
||||
RequestSendFailed,
|
||||
SynapseError,
|
||||
)
|
||||
from synapse.crypto.context_factory import FederationPolicyForHTTPS
|
||||
from synapse.http import QuieterFileBodyProducer
|
||||
from synapse.http.client import (
|
||||
BlacklistingAgentWrapper,
|
||||
BodyExceededMaxSize,
|
||||
ByteWriteable,
|
||||
_make_scheduler,
|
||||
encode_query_args,
|
||||
read_body_with_max_size,
|
||||
)
|
||||
@@ -181,7 +185,7 @@ class JsonParser(ByteParser[Union[JsonDict, list]]):
|
||||
|
||||
CONTENT_TYPE = "application/json"
|
||||
|
||||
def __init__(self):
|
||||
def __init__(self) -> None:
|
||||
self._buffer = StringIO()
|
||||
self._binary_wrapper = BinaryIOWrapper(self._buffer)
|
||||
|
||||
@@ -299,7 +303,9 @@ async def _handle_response(
|
||||
class BinaryIOWrapper:
|
||||
"""A wrapper for a TextIO which converts from bytes on the fly."""
|
||||
|
||||
def __init__(self, file: typing.TextIO, encoding="utf-8", errors="strict"):
|
||||
def __init__(
|
||||
self, file: typing.TextIO, encoding: str = "utf-8", errors: str = "strict"
|
||||
):
|
||||
self.decoder = codecs.getincrementaldecoder(encoding)(errors)
|
||||
self.file = file
|
||||
|
||||
@@ -317,7 +323,11 @@ class MatrixFederationHttpClient:
|
||||
requests.
|
||||
"""
|
||||
|
||||
def __init__(self, hs: "HomeServer", tls_client_options_factory):
|
||||
def __init__(
|
||||
self,
|
||||
hs: "HomeServer",
|
||||
tls_client_options_factory: Optional[FederationPolicyForHTTPS],
|
||||
):
|
||||
self.hs = hs
|
||||
self.signing_key = hs.signing_key
|
||||
self.server_name = hs.hostname
|
||||
@@ -348,10 +358,7 @@ class MatrixFederationHttpClient:
|
||||
self.version_string_bytes = hs.version_string.encode("ascii")
|
||||
self.default_timeout = 60
|
||||
|
||||
def schedule(x):
|
||||
self.reactor.callLater(_EPSILON, x)
|
||||
|
||||
self._cooperator = Cooperator(scheduler=schedule)
|
||||
self._cooperator = Cooperator(scheduler=_make_scheduler(self.reactor))
|
||||
|
||||
self._sleeper = AwakenableSleeper(self.reactor)
|
||||
|
||||
@@ -364,7 +371,7 @@ class MatrixFederationHttpClient:
|
||||
self,
|
||||
request: MatrixFederationRequest,
|
||||
try_trailing_slash_on_400: bool = False,
|
||||
**send_request_args,
|
||||
**send_request_args: Any,
|
||||
) -> IResponse:
|
||||
"""Wrapper for _send_request which can optionally retry the request
|
||||
upon receiving a combination of a 400 HTTP response code and a
|
||||
@@ -1159,7 +1166,7 @@ class MatrixFederationHttpClient:
|
||||
self,
|
||||
destination: str,
|
||||
path: str,
|
||||
output_stream,
|
||||
output_stream: BinaryIO,
|
||||
args: Optional[QueryParams] = None,
|
||||
retry_on_dns_fail: bool = True,
|
||||
max_size: Optional[int] = None,
|
||||
@@ -1250,10 +1257,10 @@ class MatrixFederationHttpClient:
|
||||
return length, headers
|
||||
|
||||
|
||||
def _flatten_response_never_received(e):
|
||||
def _flatten_response_never_received(e: BaseException) -> str:
|
||||
if hasattr(e, "reasons"):
|
||||
reasons = ", ".join(
|
||||
_flatten_response_never_received(f.value) for f in e.reasons
|
||||
_flatten_response_never_received(f.value) for f in e.reasons # type: ignore[attr-defined]
|
||||
)
|
||||
|
||||
return "%s:[%s]" % (type(e).__name__, reasons)
|
||||
|
||||
@@ -245,7 +245,7 @@ def http_proxy_endpoint(
|
||||
proxy: Optional[bytes],
|
||||
reactor: IReactorCore,
|
||||
tls_options_factory: Optional[IPolicyForHTTPS],
|
||||
**kwargs,
|
||||
**kwargs: object,
|
||||
) -> Tuple[Optional[IStreamClientEndpoint], Optional[ProxyCredentials]]:
|
||||
"""Parses an http proxy setting and returns an endpoint for the proxy
|
||||
|
||||
|
||||
@@ -162,7 +162,7 @@ class RequestMetrics:
|
||||
with _in_flight_requests_lock:
|
||||
_in_flight_requests.add(self)
|
||||
|
||||
def stop(self, time_sec, response_code, sent_bytes):
|
||||
def stop(self, time_sec: float, response_code: int, sent_bytes: int) -> None:
|
||||
with _in_flight_requests_lock:
|
||||
_in_flight_requests.discard(self)
|
||||
|
||||
@@ -186,13 +186,13 @@ class RequestMetrics:
|
||||
)
|
||||
return
|
||||
|
||||
response_code = str(response_code)
|
||||
response_code_str = str(response_code)
|
||||
|
||||
outgoing_responses_counter.labels(self.method, response_code).inc()
|
||||
outgoing_responses_counter.labels(self.method, response_code_str).inc()
|
||||
|
||||
response_count.labels(self.method, self.name, tag).inc()
|
||||
|
||||
response_timer.labels(self.method, self.name, tag, response_code).observe(
|
||||
response_timer.labels(self.method, self.name, tag, response_code_str).observe(
|
||||
time_sec - self.start_ts
|
||||
)
|
||||
|
||||
@@ -221,7 +221,7 @@ class RequestMetrics:
|
||||
# flight.
|
||||
self.update_metrics()
|
||||
|
||||
def update_metrics(self):
|
||||
def update_metrics(self) -> None:
|
||||
"""Updates the in flight metrics with values from this request."""
|
||||
if not self.start_context:
|
||||
logger.error(
|
||||
|
||||
@@ -33,6 +33,7 @@ from typing import (
|
||||
Optional,
|
||||
Pattern,
|
||||
Tuple,
|
||||
TypeVar,
|
||||
Union,
|
||||
)
|
||||
|
||||
@@ -92,6 +93,68 @@ HTML_ERROR_TEMPLATE = """<!DOCTYPE html>
|
||||
HTTP_STATUS_REQUEST_CANCELLED = 499
|
||||
|
||||
|
||||
F = TypeVar("F", bound=Callable[..., Any])
|
||||
|
||||
|
||||
_cancellable_method_names = frozenset(
|
||||
{
|
||||
# `RestServlet`, `BaseFederationServlet` and `BaseFederationServerServlet`
|
||||
# methods
|
||||
"on_GET",
|
||||
"on_PUT",
|
||||
"on_POST",
|
||||
"on_DELETE",
|
||||
# `_AsyncResource`, `DirectServeHtmlResource` and `DirectServeJsonResource`
|
||||
# methods
|
||||
"_async_render_GET",
|
||||
"_async_render_PUT",
|
||||
"_async_render_POST",
|
||||
"_async_render_DELETE",
|
||||
"_async_render_OPTIONS",
|
||||
# `ReplicationEndpoint` methods
|
||||
"_handle_request",
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
def cancellable(method: F) -> F:
|
||||
"""Marks a servlet method as cancellable.
|
||||
|
||||
Methods with this decorator will be cancelled if the client disconnects before we
|
||||
finish processing the request.
|
||||
|
||||
During cancellation, `Deferred.cancel()` will be invoked on the `Deferred` wrapping
|
||||
the method. The `cancel()` call will propagate down to the `Deferred` that is
|
||||
currently being waited on. That `Deferred` will raise a `CancelledError`, which will
|
||||
propagate up, as per normal exception handling.
|
||||
|
||||
Before applying this decorator to a new endpoint, you MUST recursively check
|
||||
that all `await`s in the function are on `async` functions or `Deferred`s that
|
||||
handle cancellation cleanly, otherwise a variety of bugs may occur, ranging from
|
||||
premature logging context closure, to stuck requests, to database corruption.
|
||||
|
||||
Usage:
|
||||
class SomeServlet(RestServlet):
|
||||
@cancellable
|
||||
async def on_GET(self, request: SynapseRequest) -> ...:
|
||||
...
|
||||
"""
|
||||
if method.__name__ not in _cancellable_method_names and not any(
|
||||
method.__name__.startswith(prefix) for prefix in _cancellable_method_names
|
||||
):
|
||||
raise ValueError(
|
||||
"@cancellable decorator can only be applied to servlet methods."
|
||||
)
|
||||
|
||||
method.cancellable = True # type: ignore[attr-defined]
|
||||
return method
|
||||
|
||||
|
||||
def is_method_cancellable(method: Callable[..., Any]) -> bool:
|
||||
"""Checks whether a servlet method has the `@cancellable` flag."""
|
||||
return getattr(method, "cancellable", False)
|
||||
|
||||
|
||||
def return_json_error(f: failure.Failure, request: SynapseRequest) -> None:
|
||||
"""Sends a JSON error response to clients."""
|
||||
|
||||
@@ -253,6 +316,9 @@ class HttpServer(Protocol):
|
||||
If the regex contains groups these gets passed to the callback via
|
||||
an unpacked tuple.
|
||||
|
||||
The callback may be marked with the `@cancellable` decorator, which will
|
||||
cause request processing to be cancelled when clients disconnect early.
|
||||
|
||||
Args:
|
||||
method: The HTTP method to listen to.
|
||||
path_patterns: The regex used to match requests.
|
||||
@@ -283,7 +349,9 @@ class _AsyncResource(resource.Resource, metaclass=abc.ABCMeta):
|
||||
|
||||
def render(self, request: SynapseRequest) -> int:
|
||||
"""This gets called by twisted every time someone sends us a request."""
|
||||
defer.ensureDeferred(self._async_render_wrapper(request))
|
||||
request.render_deferred = defer.ensureDeferred(
|
||||
self._async_render_wrapper(request)
|
||||
)
|
||||
return NOT_DONE_YET
|
||||
|
||||
@wrap_async_request_handler
|
||||
@@ -319,6 +387,8 @@ class _AsyncResource(resource.Resource, metaclass=abc.ABCMeta):
|
||||
|
||||
method_handler = getattr(self, "_async_render_%s" % (request_method,), None)
|
||||
if method_handler:
|
||||
request.is_render_cancellable = is_method_cancellable(method_handler)
|
||||
|
||||
raw_callback_return = method_handler(request)
|
||||
|
||||
# Is it synchronous? We'll allow this for now.
|
||||
@@ -479,6 +549,8 @@ class JsonResource(DirectServeJsonResource):
|
||||
async def _async_render(self, request: SynapseRequest) -> Tuple[int, Any]:
|
||||
callback, servlet_classname, group_dict = self._get_handler_for_request(request)
|
||||
|
||||
request.is_render_cancellable = is_method_cancellable(callback)
|
||||
|
||||
# Make sure we have an appropriate name for this handler in prometheus
|
||||
# (rather than the default of JsonResource).
|
||||
request.request_metrics.name = servlet_classname
|
||||
|
||||
@@ -19,6 +19,7 @@ from typing import TYPE_CHECKING, Any, Generator, Optional, Tuple, Union
|
||||
import attr
|
||||
from zope.interface import implementer
|
||||
|
||||
from twisted.internet.defer import Deferred
|
||||
from twisted.internet.interfaces import IAddress, IReactorTime
|
||||
from twisted.python.failure import Failure
|
||||
from twisted.web.http import HTTPChannel
|
||||
@@ -91,6 +92,14 @@ class SynapseRequest(Request):
|
||||
# we can't yet create the logcontext, as we don't know the method.
|
||||
self.logcontext: Optional[LoggingContext] = None
|
||||
|
||||
# The `Deferred` to cancel if the client disconnects early and
|
||||
# `is_render_cancellable` is set. Expected to be set by `Resource.render`.
|
||||
self.render_deferred: Optional["Deferred[None]"] = None
|
||||
# A boolean indicating whether `render_deferred` should be cancelled if the
|
||||
# client disconnects early. Expected to be set by the coroutine started by
|
||||
# `Resource.render`, if rendering is asynchronous.
|
||||
self.is_render_cancellable = False
|
||||
|
||||
global _next_request_seq
|
||||
self.request_seq = _next_request_seq
|
||||
_next_request_seq += 1
|
||||
@@ -357,7 +366,21 @@ class SynapseRequest(Request):
|
||||
{"event": "client connection lost", "reason": str(reason.value)}
|
||||
)
|
||||
|
||||
if not self._is_processing:
|
||||
if self._is_processing:
|
||||
if self.is_render_cancellable:
|
||||
if self.render_deferred is not None:
|
||||
# Throw a cancellation into the request processing, in the hope
|
||||
# that it will finish up sooner than it normally would.
|
||||
# The `self.processing()` context manager will call
|
||||
# `_finished_processing()` when done.
|
||||
with PreserveLoggingContext():
|
||||
self.render_deferred.cancel()
|
||||
else:
|
||||
logger.error(
|
||||
"Connection from client lost, but have no Deferred to "
|
||||
"cancel even though the request is marked as cancellable."
|
||||
)
|
||||
else:
|
||||
self._finished_processing()
|
||||
|
||||
def _started_processing(self, servlet_name: str) -> None:
|
||||
|
||||
@@ -31,7 +31,11 @@ from twisted.internet.endpoints import (
|
||||
TCP4ClientEndpoint,
|
||||
TCP6ClientEndpoint,
|
||||
)
|
||||
from twisted.internet.interfaces import IPushProducer, IStreamClientEndpoint
|
||||
from twisted.internet.interfaces import (
|
||||
IPushProducer,
|
||||
IReactorTCP,
|
||||
IStreamClientEndpoint,
|
||||
)
|
||||
from twisted.internet.protocol import Factory, Protocol
|
||||
from twisted.internet.tcp import Connection
|
||||
from twisted.python.failure import Failure
|
||||
@@ -59,14 +63,14 @@ class LogProducer:
|
||||
_buffer: Deque[logging.LogRecord]
|
||||
_paused: bool = attr.ib(default=False, init=False)
|
||||
|
||||
def pauseProducing(self):
|
||||
def pauseProducing(self) -> None:
|
||||
self._paused = True
|
||||
|
||||
def stopProducing(self):
|
||||
def stopProducing(self) -> None:
|
||||
self._paused = True
|
||||
self._buffer = deque()
|
||||
|
||||
def resumeProducing(self):
|
||||
def resumeProducing(self) -> None:
|
||||
# If we're already producing, nothing to do.
|
||||
self._paused = False
|
||||
|
||||
@@ -102,8 +106,8 @@ class RemoteHandler(logging.Handler):
|
||||
host: str,
|
||||
port: int,
|
||||
maximum_buffer: int = 1000,
|
||||
level=logging.NOTSET,
|
||||
_reactor=None,
|
||||
level: int = logging.NOTSET,
|
||||
_reactor: Optional[IReactorTCP] = None,
|
||||
):
|
||||
super().__init__(level=level)
|
||||
self.host = host
|
||||
@@ -118,7 +122,7 @@ class RemoteHandler(logging.Handler):
|
||||
if _reactor is None:
|
||||
from twisted.internet import reactor
|
||||
|
||||
_reactor = reactor
|
||||
_reactor = reactor # type: ignore[assignment]
|
||||
|
||||
try:
|
||||
ip = ip_address(self.host)
|
||||
@@ -139,7 +143,7 @@ class RemoteHandler(logging.Handler):
|
||||
self._stopping = False
|
||||
self._connect()
|
||||
|
||||
def close(self):
|
||||
def close(self) -> None:
|
||||
self._stopping = True
|
||||
self._service.stopService()
|
||||
|
||||
|
||||
@@ -16,6 +16,8 @@
|
||||
import logging
|
||||
import traceback
|
||||
from io import StringIO
|
||||
from types import TracebackType
|
||||
from typing import Optional, Tuple, Type
|
||||
|
||||
|
||||
class LogFormatter(logging.Formatter):
|
||||
@@ -28,10 +30,14 @@ class LogFormatter(logging.Formatter):
|
||||
where it was caught are logged).
|
||||
"""
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
def formatException(self, ei):
|
||||
def formatException(
|
||||
self,
|
||||
ei: Tuple[
|
||||
Optional[Type[BaseException]],
|
||||
Optional[BaseException],
|
||||
Optional[TracebackType],
|
||||
],
|
||||
) -> str:
|
||||
sio = StringIO()
|
||||
(typ, val, tb) = ei
|
||||
|
||||
|
||||
@@ -49,7 +49,7 @@ class PeriodicallyFlushingMemoryHandler(MemoryHandler):
|
||||
)
|
||||
self._flushing_thread.start()
|
||||
|
||||
def on_reactor_running():
|
||||
def on_reactor_running() -> None:
|
||||
self._reactor_started = True
|
||||
|
||||
reactor_to_use: IReactorCore
|
||||
@@ -74,7 +74,7 @@ class PeriodicallyFlushingMemoryHandler(MemoryHandler):
|
||||
else:
|
||||
return True
|
||||
|
||||
def _flush_periodically(self):
|
||||
def _flush_periodically(self) -> None:
|
||||
"""
|
||||
Whilst this handler is active, flush the handler periodically.
|
||||
"""
|
||||
|
||||
@@ -13,6 +13,8 @@
|
||||
# limitations under the License.import logging
|
||||
|
||||
import logging
|
||||
from types import TracebackType
|
||||
from typing import Optional, Type
|
||||
|
||||
from opentracing import Scope, ScopeManager
|
||||
|
||||
@@ -107,19 +109,26 @@ class _LogContextScope(Scope):
|
||||
and - if enter_logcontext was set - the logcontext is finished too.
|
||||
"""
|
||||
|
||||
def __init__(self, manager, span, logcontext, enter_logcontext, finish_on_close):
|
||||
def __init__(
|
||||
self,
|
||||
manager: LogContextScopeManager,
|
||||
span,
|
||||
logcontext,
|
||||
enter_logcontext: bool,
|
||||
finish_on_close: bool,
|
||||
):
|
||||
"""
|
||||
Args:
|
||||
manager (LogContextScopeManager):
|
||||
manager:
|
||||
the manager that is responsible for this scope.
|
||||
span (Span):
|
||||
the opentracing span which this scope represents the local
|
||||
lifetime for.
|
||||
logcontext (LogContext):
|
||||
the logcontext to which this scope is attached.
|
||||
enter_logcontext (Boolean):
|
||||
enter_logcontext:
|
||||
if True the logcontext will be exited when the scope is finished
|
||||
finish_on_close (Boolean):
|
||||
finish_on_close:
|
||||
if True finish the span when the scope is closed
|
||||
"""
|
||||
super().__init__(manager, span)
|
||||
@@ -127,16 +136,21 @@ class _LogContextScope(Scope):
|
||||
self._finish_on_close = finish_on_close
|
||||
self._enter_logcontext = enter_logcontext
|
||||
|
||||
def __exit__(self, exc_type, value, traceback):
|
||||
def __exit__(
|
||||
self,
|
||||
exc_type: Optional[Type[BaseException]],
|
||||
value: Optional[BaseException],
|
||||
traceback: Optional[TracebackType],
|
||||
) -> None:
|
||||
if exc_type == twisted.internet.defer._DefGen_Return:
|
||||
# filter out defer.returnValue() calls
|
||||
exc_type = value = traceback = None
|
||||
super().__exit__(exc_type, value, traceback)
|
||||
|
||||
def __str__(self):
|
||||
def __str__(self) -> str:
|
||||
return f"Scope<{self.span}>"
|
||||
|
||||
def close(self):
|
||||
def close(self) -> None:
|
||||
active_scope = self.manager.active
|
||||
if active_scope is not self:
|
||||
logger.error(
|
||||
|
||||
@@ -18,6 +18,7 @@ import os
|
||||
import re
|
||||
from typing import Iterable, Optional, overload
|
||||
|
||||
import attr
|
||||
from prometheus_client import REGISTRY, Metric
|
||||
from typing_extensions import Literal
|
||||
|
||||
@@ -27,52 +28,24 @@ from synapse.metrics._types import Collector
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def _setup_jemalloc_stats() -> None:
|
||||
"""Checks to see if jemalloc is loaded, and hooks up a collector to record
|
||||
statistics exposed by jemalloc.
|
||||
"""
|
||||
|
||||
# Try to find the loaded jemalloc shared library, if any. We need to
|
||||
# introspect into what is loaded, rather than loading whatever is on the
|
||||
# path, as if we load a *different* jemalloc version things will seg fault.
|
||||
|
||||
# We look in `/proc/self/maps`, which only exists on linux.
|
||||
if not os.path.exists("/proc/self/maps"):
|
||||
logger.debug("Not looking for jemalloc as no /proc/self/maps exist")
|
||||
return
|
||||
|
||||
# We're looking for a path at the end of the line that includes
|
||||
# "libjemalloc".
|
||||
regex = re.compile(r"/\S+/libjemalloc.*$")
|
||||
|
||||
jemalloc_path = None
|
||||
with open("/proc/self/maps") as f:
|
||||
for line in f:
|
||||
match = regex.search(line.strip())
|
||||
if match:
|
||||
jemalloc_path = match.group()
|
||||
|
||||
if not jemalloc_path:
|
||||
# No loaded jemalloc was found.
|
||||
logger.debug("jemalloc not found")
|
||||
return
|
||||
|
||||
logger.debug("Found jemalloc at %s", jemalloc_path)
|
||||
|
||||
jemalloc = ctypes.CDLL(jemalloc_path)
|
||||
@attr.s(slots=True, frozen=True, auto_attribs=True)
|
||||
class JemallocStats:
|
||||
jemalloc: ctypes.CDLL
|
||||
|
||||
@overload
|
||||
def _mallctl(
|
||||
name: str, read: Literal[True] = True, write: Optional[int] = None
|
||||
self, name: str, read: Literal[True] = True, write: Optional[int] = None
|
||||
) -> int:
|
||||
...
|
||||
|
||||
@overload
|
||||
def _mallctl(name: str, read: Literal[False], write: Optional[int] = None) -> None:
|
||||
def _mallctl(
|
||||
self, name: str, read: Literal[False], write: Optional[int] = None
|
||||
) -> None:
|
||||
...
|
||||
|
||||
def _mallctl(
|
||||
name: str, read: bool = True, write: Optional[int] = None
|
||||
self, name: str, read: bool = True, write: Optional[int] = None
|
||||
) -> Optional[int]:
|
||||
"""Wrapper around `mallctl` for reading and writing integers to
|
||||
jemalloc.
|
||||
@@ -120,7 +93,7 @@ def _setup_jemalloc_stats() -> None:
|
||||
# Where oldp/oldlenp is a buffer where the old value will be written to
|
||||
# (if not null), and newp/newlen is the buffer with the new value to set
|
||||
# (if not null). Note that they're all references *except* newlen.
|
||||
result = jemalloc.mallctl(
|
||||
result = self.jemalloc.mallctl(
|
||||
name.encode("ascii"),
|
||||
input_var_ref,
|
||||
input_len_ref,
|
||||
@@ -136,21 +109,80 @@ def _setup_jemalloc_stats() -> None:
|
||||
|
||||
return input_var.value
|
||||
|
||||
def _jemalloc_refresh_stats() -> None:
|
||||
def refresh_stats(self) -> None:
|
||||
"""Request that jemalloc updates its internal statistics. This needs to
|
||||
be called before querying for stats, otherwise it will return stale
|
||||
values.
|
||||
"""
|
||||
try:
|
||||
_mallctl("epoch", read=False, write=1)
|
||||
self._mallctl("epoch", read=False, write=1)
|
||||
except Exception as e:
|
||||
logger.warning("Failed to reload jemalloc stats: %s", e)
|
||||
|
||||
def get_stat(self, name: str) -> int:
|
||||
"""Request the stat of the given name at the time of the last
|
||||
`refresh_stats` call. This may throw if we fail to read
|
||||
the stat.
|
||||
"""
|
||||
return self._mallctl(f"stats.{name}")
|
||||
|
||||
|
||||
_JEMALLOC_STATS: Optional[JemallocStats] = None
|
||||
|
||||
|
||||
def get_jemalloc_stats() -> Optional[JemallocStats]:
|
||||
"""Returns an interface to jemalloc, if it is being used.
|
||||
|
||||
Note that this will always return None until `setup_jemalloc_stats` has been
|
||||
called.
|
||||
"""
|
||||
return _JEMALLOC_STATS
|
||||
|
||||
|
||||
def _setup_jemalloc_stats() -> None:
|
||||
"""Checks to see if jemalloc is loaded, and hooks up a collector to record
|
||||
statistics exposed by jemalloc.
|
||||
"""
|
||||
|
||||
global _JEMALLOC_STATS
|
||||
|
||||
# Try to find the loaded jemalloc shared library, if any. We need to
|
||||
# introspect into what is loaded, rather than loading whatever is on the
|
||||
# path, as if we load a *different* jemalloc version things will seg fault.
|
||||
|
||||
# We look in `/proc/self/maps`, which only exists on linux.
|
||||
if not os.path.exists("/proc/self/maps"):
|
||||
logger.debug("Not looking for jemalloc as no /proc/self/maps exist")
|
||||
return
|
||||
|
||||
# We're looking for a path at the end of the line that includes
|
||||
# "libjemalloc".
|
||||
regex = re.compile(r"/\S+/libjemalloc.*$")
|
||||
|
||||
jemalloc_path = None
|
||||
with open("/proc/self/maps") as f:
|
||||
for line in f:
|
||||
match = regex.search(line.strip())
|
||||
if match:
|
||||
jemalloc_path = match.group()
|
||||
|
||||
if not jemalloc_path:
|
||||
# No loaded jemalloc was found.
|
||||
logger.debug("jemalloc not found")
|
||||
return
|
||||
|
||||
logger.debug("Found jemalloc at %s", jemalloc_path)
|
||||
|
||||
jemalloc_dll = ctypes.CDLL(jemalloc_path)
|
||||
|
||||
stats = JemallocStats(jemalloc_dll)
|
||||
_JEMALLOC_STATS = stats
|
||||
|
||||
class JemallocCollector(Collector):
|
||||
"""Metrics for internal jemalloc stats."""
|
||||
|
||||
def collect(self) -> Iterable[Metric]:
|
||||
_jemalloc_refresh_stats()
|
||||
stats.refresh_stats()
|
||||
|
||||
g = GaugeMetricFamily(
|
||||
"jemalloc_stats_app_memory_bytes",
|
||||
@@ -184,7 +216,7 @@ def _setup_jemalloc_stats() -> None:
|
||||
"metadata",
|
||||
):
|
||||
try:
|
||||
value = _mallctl(f"stats.{t}")
|
||||
value = stats.get_stat(t)
|
||||
except Exception as e:
|
||||
# There was an error fetching the value, skip.
|
||||
logger.warning("Failed to read jemalloc stats.%s: %s", t, e)
|
||||
|
||||
@@ -46,6 +46,7 @@ from synapse.types import (
|
||||
JsonDict,
|
||||
PersistedEventPosition,
|
||||
RoomStreamToken,
|
||||
StreamKeyType,
|
||||
StreamToken,
|
||||
UserID,
|
||||
)
|
||||
@@ -370,7 +371,7 @@ class Notifier:
|
||||
|
||||
if users or rooms:
|
||||
self.on_new_event(
|
||||
"room_key",
|
||||
StreamKeyType.ROOM,
|
||||
max_room_stream_token,
|
||||
users=users,
|
||||
rooms=rooms,
|
||||
@@ -440,7 +441,7 @@ class Notifier:
|
||||
for room in rooms:
|
||||
user_streams |= self.room_to_user_streams.get(room, set())
|
||||
|
||||
if stream_key == "to_device_key":
|
||||
if stream_key == StreamKeyType.TO_DEVICE:
|
||||
issue9533_logger.debug(
|
||||
"to-device messages stream id %s, awaking streams for %s",
|
||||
new_token,
|
||||
|
||||
@@ -12,6 +12,80 @@
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
"""
|
||||
This module implements the push rules & notifications portion of the Matrix
|
||||
specification.
|
||||
|
||||
There's a few related features:
|
||||
|
||||
* Push notifications (i.e. email or outgoing requests to a Push Gateway).
|
||||
* Calculation of unread notifications (for /sync and /notifications).
|
||||
|
||||
When Synapse receives a new event (locally, via the Client-Server API, or via
|
||||
federation), the following occurs:
|
||||
|
||||
1. The push rules get evaluated to generate a set of per-user actions.
|
||||
2. The event is persisted into the database.
|
||||
3. (In the background) The notifier is notified about the new event.
|
||||
|
||||
The per-user actions are initially stored in the event_push_actions_staging table,
|
||||
before getting moved into the event_push_actions table when the event is persisted.
|
||||
The event_push_actions table is periodically summarised into the event_push_summary
|
||||
and event_push_summary_stream_ordering tables.
|
||||
|
||||
Since push actions block an event from being persisted the generation of push
|
||||
actions is performance sensitive.
|
||||
|
||||
The general interaction of the classes are:
|
||||
|
||||
+---------------------------------------------+
|
||||
| FederationEventHandler/EventCreationHandler |
|
||||
+---------------------------------------------+
|
||||
|
|
||||
v
|
||||
+-----------------------+ +---------------------------+
|
||||
| BulkPushRuleEvaluator |---->| PushRuleEvaluatorForEvent |
|
||||
+-----------------------+ +---------------------------+
|
||||
|
|
||||
v
|
||||
+-----------------------------+
|
||||
| EventPushActionsWorkerStore |
|
||||
+-----------------------------+
|
||||
|
||||
The notifier notifies the pusher pool of the new event, which checks for affected
|
||||
users. Each user-configured pusher of the affected users then performs the
|
||||
previously calculated action.
|
||||
|
||||
The general interaction of the classes are:
|
||||
|
||||
+----------+
|
||||
| Notifier |
|
||||
+----------+
|
||||
|
|
||||
v
|
||||
+------------+ +--------------+
|
||||
| PusherPool |---->| PusherConfig |
|
||||
+------------+ +--------------+
|
||||
|
|
||||
| +---------------+
|
||||
+<--->| PusherFactory |
|
||||
| +---------------+
|
||||
v
|
||||
+------------------------+ +-----------------------------------------------+
|
||||
| EmailPusher/HttpPusher |---->| EventPushActionsWorkerStore/PusherWorkerStore |
|
||||
+------------------------+ +-----------------------------------------------+
|
||||
|
|
||||
v
|
||||
+-------------------------+
|
||||
| Mailer/SimpleHttpClient |
|
||||
+-------------------------+
|
||||
|
||||
The Pusher instance also calls out to various utilities for generating payloads
|
||||
(or email templates), but those interactions are not detailed in this diagram
|
||||
(and are specific to the type of pusher).
|
||||
|
||||
"""
|
||||
|
||||
import abc
|
||||
from typing import TYPE_CHECKING, Any, Dict, Optional
|
||||
|
||||
|
||||
@@ -1,44 +0,0 @@
|
||||
# Copyright 2015 OpenMarket Ltd
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
import logging
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from synapse.events import EventBase
|
||||
from synapse.events.snapshot import EventContext
|
||||
from synapse.push.bulk_push_rule_evaluator import BulkPushRuleEvaluator
|
||||
from synapse.util.metrics import Measure
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from synapse.server import HomeServer
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class ActionGenerator:
|
||||
def __init__(self, hs: "HomeServer"):
|
||||
self.clock = hs.get_clock()
|
||||
self.bulk_evaluator = BulkPushRuleEvaluator(hs)
|
||||
# really we want to get all user ids and all profile tags too,
|
||||
# since we want the actions for each profile tag for every user and
|
||||
# also actions for a client with no profile tag for each user.
|
||||
# Currently the event stream doesn't support profile tags on an
|
||||
# event stream, so we just run the rules for a client with no profile
|
||||
# tag (ie. we just need all the users).
|
||||
|
||||
async def handle_push_actions_for_event(
|
||||
self, event: EventBase, context: EventContext
|
||||
) -> None:
|
||||
with Measure(self.clock, "action_for_event_by_user"):
|
||||
await self.bulk_evaluator.action_for_event_by_user(event, context)
|
||||
@@ -21,7 +21,7 @@ from prometheus_client import Counter
|
||||
|
||||
from synapse.api.constants import EventTypes, Membership, RelationTypes
|
||||
from synapse.event_auth import get_user_power_level
|
||||
from synapse.events import EventBase
|
||||
from synapse.events import EventBase, relation_from_event
|
||||
from synapse.events.snapshot import EventContext
|
||||
from synapse.state import POWER_KEY
|
||||
from synapse.storage.databases.main.roommember import EventIdMembership
|
||||
@@ -29,6 +29,7 @@ from synapse.util.async_helpers import Linearizer
|
||||
from synapse.util.caches import CacheMetric, register_cache
|
||||
from synapse.util.caches.descriptors import lru_cache
|
||||
from synapse.util.caches.lrucache import LruCache
|
||||
from synapse.util.metrics import measure_func
|
||||
|
||||
from .push_rule_evaluator import PushRuleEvaluatorForEvent
|
||||
|
||||
@@ -77,8 +78,8 @@ def _should_count_as_unread(event: EventBase, context: EventContext) -> bool:
|
||||
return False
|
||||
|
||||
# Exclude edits.
|
||||
relates_to = event.content.get("m.relates_to", {})
|
||||
if relates_to.get("rel_type") == RelationTypes.REPLACE:
|
||||
relates_to = relation_from_event(event)
|
||||
if relates_to and relates_to.rel_type == RelationTypes.REPLACE:
|
||||
return False
|
||||
|
||||
# Mark events that have a non-empty string body as unread.
|
||||
@@ -105,6 +106,7 @@ class BulkPushRuleEvaluator:
|
||||
def __init__(self, hs: "HomeServer"):
|
||||
self.hs = hs
|
||||
self.store = hs.get_datastores().main
|
||||
self.clock = hs.get_clock()
|
||||
self._event_auth_handler = hs.get_event_auth_handler()
|
||||
|
||||
# Used by `RulesForRoom` to ensure only one thing mutates the cache at a
|
||||
@@ -185,6 +187,7 @@ class BulkPushRuleEvaluator:
|
||||
|
||||
return pl_event.content if pl_event else {}, sender_level
|
||||
|
||||
@measure_func("action_for_event_by_user")
|
||||
async def action_for_event_by_user(
|
||||
self, event: EventBase, context: EventContext
|
||||
) -> None:
|
||||
@@ -192,6 +195,10 @@ class BulkPushRuleEvaluator:
|
||||
should increment the unread count, and insert the results into the
|
||||
event_push_actions_staging table.
|
||||
"""
|
||||
if event.internal_metadata.is_outlier():
|
||||
# This can happen due to out of band memberships
|
||||
return
|
||||
|
||||
count_as_unread = _should_count_as_unread(event, context)
|
||||
|
||||
rules_by_user = await self._get_rules_for_event(event, context)
|
||||
@@ -208,8 +215,6 @@ class BulkPushRuleEvaluator:
|
||||
event, len(room_members), sender_power_level, power_levels
|
||||
)
|
||||
|
||||
condition_cache: Dict[str, bool] = {}
|
||||
|
||||
# If the event is not a state event check if any users ignore the sender.
|
||||
if not event.is_state():
|
||||
ignorers = await self.store.ignored_by(event.sender)
|
||||
@@ -247,8 +252,8 @@ class BulkPushRuleEvaluator:
|
||||
if "enabled" in rule and not rule["enabled"]:
|
||||
continue
|
||||
|
||||
matches = _condition_checker(
|
||||
evaluator, rule["conditions"], uid, display_name, condition_cache
|
||||
matches = evaluator.check_conditions(
|
||||
rule["conditions"], uid, display_name
|
||||
)
|
||||
if matches:
|
||||
actions = [x for x in rule["actions"] if x != "dont_notify"]
|
||||
@@ -267,32 +272,6 @@ class BulkPushRuleEvaluator:
|
||||
)
|
||||
|
||||
|
||||
def _condition_checker(
|
||||
evaluator: PushRuleEvaluatorForEvent,
|
||||
conditions: List[dict],
|
||||
uid: str,
|
||||
display_name: Optional[str],
|
||||
cache: Dict[str, bool],
|
||||
) -> bool:
|
||||
for cond in conditions:
|
||||
_cache_key = cond.get("_cache_key", None)
|
||||
if _cache_key:
|
||||
res = cache.get(_cache_key, None)
|
||||
if res is False:
|
||||
return False
|
||||
elif res is True:
|
||||
continue
|
||||
|
||||
res = evaluator.matches(cond, uid, display_name)
|
||||
if _cache_key:
|
||||
cache[_cache_key] = bool(res)
|
||||
|
||||
if not res:
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
|
||||
MemberMap = Dict[str, Optional[EventIdMembership]]
|
||||
Rule = Dict[str, dict]
|
||||
RulesByUser = Dict[str, List[Rule]]
|
||||
|
||||
@@ -129,9 +129,55 @@ class PushRuleEvaluatorForEvent:
|
||||
# Maps strings of e.g. 'content.body' -> event["content"]["body"]
|
||||
self._value_cache = _flatten_dict(event)
|
||||
|
||||
# Maps cache keys to final values.
|
||||
self._condition_cache: Dict[str, bool] = {}
|
||||
|
||||
def check_conditions(
|
||||
self, conditions: List[dict], uid: str, display_name: Optional[str]
|
||||
) -> bool:
|
||||
"""
|
||||
Returns true if a user's conditions/user ID/display name match the event.
|
||||
|
||||
Args:
|
||||
conditions: The user's conditions to match.
|
||||
uid: The user's MXID.
|
||||
display_name: The display name.
|
||||
|
||||
Returns:
|
||||
True if all conditions match the event, False otherwise.
|
||||
"""
|
||||
for cond in conditions:
|
||||
_cache_key = cond.get("_cache_key", None)
|
||||
if _cache_key:
|
||||
res = self._condition_cache.get(_cache_key, None)
|
||||
if res is False:
|
||||
return False
|
||||
elif res is True:
|
||||
continue
|
||||
|
||||
res = self.matches(cond, uid, display_name)
|
||||
if _cache_key:
|
||||
self._condition_cache[_cache_key] = bool(res)
|
||||
|
||||
if not res:
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
def matches(
|
||||
self, condition: Dict[str, Any], user_id: str, display_name: Optional[str]
|
||||
) -> bool:
|
||||
"""
|
||||
Returns true if a user's condition/user ID/display name match the event.
|
||||
|
||||
Args:
|
||||
condition: The user's condition to match.
|
||||
uid: The user's MXID.
|
||||
display_name: The display name, or None if there is not one.
|
||||
|
||||
Returns:
|
||||
True if the condition matches the event, False otherwise.
|
||||
"""
|
||||
if condition["kind"] == "event_match":
|
||||
return self._event_match(condition, user_id)
|
||||
elif condition["kind"] == "contains_display_name":
|
||||
@@ -146,6 +192,16 @@ class PushRuleEvaluatorForEvent:
|
||||
return True
|
||||
|
||||
def _event_match(self, condition: dict, user_id: str) -> bool:
|
||||
"""
|
||||
Check an "event_match" push rule condition.
|
||||
|
||||
Args:
|
||||
condition: The "event_match" push rule condition to match.
|
||||
user_id: The user's MXID.
|
||||
|
||||
Returns:
|
||||
True if the condition matches the event, False otherwise.
|
||||
"""
|
||||
pattern = condition.get("pattern", None)
|
||||
|
||||
if not pattern:
|
||||
@@ -167,13 +223,22 @@ class PushRuleEvaluatorForEvent:
|
||||
|
||||
return _glob_matches(pattern, body, word_boundary=True)
|
||||
else:
|
||||
haystack = self._get_value(condition["key"])
|
||||
haystack = self._value_cache.get(condition["key"], None)
|
||||
if haystack is None:
|
||||
return False
|
||||
|
||||
return _glob_matches(pattern, haystack)
|
||||
|
||||
def _contains_display_name(self, display_name: Optional[str]) -> bool:
|
||||
"""
|
||||
Check an "event_match" push rule condition.
|
||||
|
||||
Args:
|
||||
display_name: The display name, or None if there is not one.
|
||||
|
||||
Returns:
|
||||
True if the display name is found in the event body, False otherwise.
|
||||
"""
|
||||
if not display_name:
|
||||
return False
|
||||
|
||||
@@ -191,9 +256,6 @@ class PushRuleEvaluatorForEvent:
|
||||
|
||||
return bool(r.search(body))
|
||||
|
||||
def _get_value(self, dotted_key: str) -> Optional[str]:
|
||||
return self._value_cache.get(dotted_key, None)
|
||||
|
||||
|
||||
# Caches (string, is_glob, word_boundary) -> regex for push. See _glob_matches
|
||||
regex_cache: LruCache[Tuple[str, bool, bool], Pattern] = LruCache(
|
||||
|
||||
@@ -26,7 +26,8 @@ from twisted.web.server import Request
|
||||
|
||||
from synapse.api.errors import HttpResponseException, SynapseError
|
||||
from synapse.http import RequestTimedOutError
|
||||
from synapse.http.server import HttpServer
|
||||
from synapse.http.server import HttpServer, is_method_cancellable
|
||||
from synapse.http.site import SynapseRequest
|
||||
from synapse.logging import opentracing
|
||||
from synapse.logging.opentracing import trace
|
||||
from synapse.types import JsonDict
|
||||
@@ -310,6 +311,12 @@ class ReplicationEndpoint(metaclass=abc.ABCMeta):
|
||||
url_args = list(self.PATH_ARGS)
|
||||
method = self.METHOD
|
||||
|
||||
if self.CACHE and is_method_cancellable(self._handle_request):
|
||||
raise Exception(
|
||||
f"{self.__class__.__name__} has been marked as cancellable, but CACHE "
|
||||
"is set. The cancellable flag would have no effect."
|
||||
)
|
||||
|
||||
if self.CACHE:
|
||||
url_args.append("txn_id")
|
||||
|
||||
@@ -324,7 +331,7 @@ class ReplicationEndpoint(metaclass=abc.ABCMeta):
|
||||
)
|
||||
|
||||
async def _check_auth_and_handle(
|
||||
self, request: Request, **kwargs: Any
|
||||
self, request: SynapseRequest, **kwargs: Any
|
||||
) -> Tuple[int, JsonDict]:
|
||||
"""Called on new incoming requests when caching is enabled. Checks
|
||||
if there is a cached response for the request and returns that,
|
||||
@@ -340,8 +347,18 @@ class ReplicationEndpoint(metaclass=abc.ABCMeta):
|
||||
if self.CACHE:
|
||||
txn_id = kwargs.pop("txn_id")
|
||||
|
||||
# We ignore the `@cancellable` flag, since cancellation wouldn't interupt
|
||||
# `_handle_request` and `ResponseCache` does not handle cancellation
|
||||
# correctly yet. In particular, there may be issues to do with logging
|
||||
# context lifetimes.
|
||||
|
||||
return await self.response_cache.wrap(
|
||||
txn_id, self._handle_request, request, **kwargs
|
||||
)
|
||||
|
||||
# The `@cancellable` decorator may be applied to `_handle_request`. But we
|
||||
# told `HttpServer.register_paths` that our handler is `_check_auth_and_handle`,
|
||||
# so we have to set up the cancellable flag ourselves.
|
||||
request.is_render_cancellable = is_method_cancellable(self._handle_request)
|
||||
|
||||
return await self._handle_request(request, **kwargs)
|
||||
|
||||
@@ -43,7 +43,7 @@ from synapse.replication.tcp.streams.events import (
|
||||
EventsStreamEventRow,
|
||||
EventsStreamRow,
|
||||
)
|
||||
from synapse.types import PersistedEventPosition, ReadReceipt, UserID
|
||||
from synapse.types import PersistedEventPosition, ReadReceipt, StreamKeyType, UserID
|
||||
from synapse.util.async_helpers import Linearizer, timeout_deferred
|
||||
from synapse.util.metrics import Measure
|
||||
|
||||
@@ -153,19 +153,19 @@ class ReplicationDataHandler:
|
||||
if stream_name == TypingStream.NAME:
|
||||
self._typing_handler.process_replication_rows(token, rows)
|
||||
self.notifier.on_new_event(
|
||||
"typing_key", token, rooms=[row.room_id for row in rows]
|
||||
StreamKeyType.TYPING, token, rooms=[row.room_id for row in rows]
|
||||
)
|
||||
elif stream_name == PushRulesStream.NAME:
|
||||
self.notifier.on_new_event(
|
||||
"push_rules_key", token, users=[row.user_id for row in rows]
|
||||
StreamKeyType.PUSH_RULES, token, users=[row.user_id for row in rows]
|
||||
)
|
||||
elif stream_name in (AccountDataStream.NAME, TagAccountDataStream.NAME):
|
||||
self.notifier.on_new_event(
|
||||
"account_data_key", token, users=[row.user_id for row in rows]
|
||||
StreamKeyType.ACCOUNT_DATA, token, users=[row.user_id for row in rows]
|
||||
)
|
||||
elif stream_name == ReceiptsStream.NAME:
|
||||
self.notifier.on_new_event(
|
||||
"receipt_key", token, rooms=[row.room_id for row in rows]
|
||||
StreamKeyType.RECEIPT, token, rooms=[row.room_id for row in rows]
|
||||
)
|
||||
await self._pusher_pool.on_new_receipts(
|
||||
token, token, {row.room_id for row in rows}
|
||||
@@ -173,14 +173,18 @@ class ReplicationDataHandler:
|
||||
elif stream_name == ToDeviceStream.NAME:
|
||||
entities = [row.entity for row in rows if row.entity.startswith("@")]
|
||||
if entities:
|
||||
self.notifier.on_new_event("to_device_key", token, users=entities)
|
||||
self.notifier.on_new_event(
|
||||
StreamKeyType.TO_DEVICE, token, users=entities
|
||||
)
|
||||
elif stream_name == DeviceListsStream.NAME:
|
||||
all_room_ids: Set[str] = set()
|
||||
for row in rows:
|
||||
if row.entity.startswith("@"):
|
||||
room_ids = await self.store.get_rooms_for_user(row.entity)
|
||||
all_room_ids.update(room_ids)
|
||||
self.notifier.on_new_event("device_list_key", token, rooms=all_room_ids)
|
||||
self.notifier.on_new_event(
|
||||
StreamKeyType.DEVICE_LIST, token, rooms=all_room_ids
|
||||
)
|
||||
elif stream_name == GroupServerStream.NAME:
|
||||
self.notifier.on_new_event(
|
||||
"groups_key", token, users=[row.user_id for row in rows]
|
||||
|
||||
@@ -13,12 +13,10 @@
|
||||
# limitations under the License.
|
||||
|
||||
import logging
|
||||
import re
|
||||
from typing import TYPE_CHECKING, Tuple
|
||||
|
||||
from synapse.api.constants import ReceiptTypes
|
||||
from synapse.api.errors import SynapseError
|
||||
from synapse.http import get_request_user_agent
|
||||
from synapse.http.server import HttpServer
|
||||
from synapse.http.servlet import RestServlet, parse_json_object_from_request
|
||||
from synapse.http.site import SynapseRequest
|
||||
@@ -26,8 +24,6 @@ from synapse.types import JsonDict
|
||||
|
||||
from ._base import client_patterns
|
||||
|
||||
pattern = re.compile(r"(?:Element|SchildiChat)/1\.[012]\.")
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from synapse.server import HomeServer
|
||||
|
||||
@@ -69,14 +65,7 @@ class ReceiptRestServlet(RestServlet):
|
||||
):
|
||||
raise SynapseError(400, "Receipt type must be 'm.read'")
|
||||
|
||||
# Do not allow older SchildiChat and Element Android clients (prior to Element/1.[012].x) to send an empty body.
|
||||
user_agent = get_request_user_agent(request)
|
||||
allow_empty_body = False
|
||||
if "Android" in user_agent:
|
||||
if pattern.match(user_agent) or "Riot" in user_agent:
|
||||
allow_empty_body = True
|
||||
# This call makes sure possible empty body is handled correctly
|
||||
parse_json_object_from_request(request, allow_empty_body)
|
||||
parse_json_object_from_request(request, allow_empty_body=False)
|
||||
|
||||
await self.presence_handler.bump_presence_active_time(requester.user)
|
||||
|
||||
|
||||
@@ -34,7 +34,7 @@ from synapse.api.errors import (
|
||||
)
|
||||
from synapse.api.filtering import Filter
|
||||
from synapse.events.utils import format_event_for_client_v2
|
||||
from synapse.http.server import HttpServer
|
||||
from synapse.http.server import HttpServer, cancellable
|
||||
from synapse.http.servlet import (
|
||||
ResolveRoomIdMixin,
|
||||
RestServlet,
|
||||
@@ -109,10 +109,10 @@ class RoomStateEventRestServlet(TransactionRestServlet):
|
||||
self.auth = hs.get_auth()
|
||||
|
||||
def register(self, http_server: HttpServer) -> None:
|
||||
# /room/$roomid/state/$eventtype
|
||||
# /rooms/$roomid/state/$eventtype
|
||||
no_state_key = "/rooms/(?P<room_id>[^/]*)/state/(?P<event_type>[^/]*)$"
|
||||
|
||||
# /room/$roomid/state/$eventtype/$statekey
|
||||
# /rooms/$roomid/state/$eventtype/$statekey
|
||||
state_key = (
|
||||
"/rooms/(?P<room_id>[^/]*)/state/"
|
||||
"(?P<event_type>[^/]*)/(?P<state_key>[^/]*)$"
|
||||
@@ -143,6 +143,7 @@ class RoomStateEventRestServlet(TransactionRestServlet):
|
||||
self.__class__.__name__,
|
||||
)
|
||||
|
||||
@cancellable
|
||||
def on_GET_no_state_key(
|
||||
self, request: SynapseRequest, room_id: str, event_type: str
|
||||
) -> Awaitable[Tuple[int, JsonDict]]:
|
||||
@@ -153,6 +154,7 @@ class RoomStateEventRestServlet(TransactionRestServlet):
|
||||
) -> Awaitable[Tuple[int, JsonDict]]:
|
||||
return self.on_PUT(request, room_id, event_type, "")
|
||||
|
||||
@cancellable
|
||||
async def on_GET(
|
||||
self, request: SynapseRequest, room_id: str, event_type: str, state_key: str
|
||||
) -> Tuple[int, JsonDict]:
|
||||
@@ -481,6 +483,7 @@ class RoomMemberListRestServlet(RestServlet):
|
||||
self.auth = hs.get_auth()
|
||||
self.store = hs.get_datastores().main
|
||||
|
||||
@cancellable
|
||||
async def on_GET(
|
||||
self, request: SynapseRequest, room_id: str
|
||||
) -> Tuple[int, JsonDict]:
|
||||
@@ -602,6 +605,7 @@ class RoomStateRestServlet(RestServlet):
|
||||
self.message_handler = hs.get_message_handler()
|
||||
self.auth = hs.get_auth()
|
||||
|
||||
@cancellable
|
||||
async def on_GET(
|
||||
self, request: SynapseRequest, room_id: str
|
||||
) -> Tuple[int, List[JsonDict]]:
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user